How to Batch Update X Account Profiles with TwexAPI
If you only manage one X account, updating the profile by hand is usually fine. The problem starts when a team maintains a group of accounts: brand handles, regional support accounts, product accounts, or short-lived campaign profiles. Names, bios, locations, avatars, and banners need to stay consistent, and one missed field can leave old campaign copy live for weeks.
TwexAPI's profile update endpoint is useful for that kind of repeatable maintenance. The safest pattern is to validate one account first, then move the same request into a small-concurrency batch script, and keep a result file for every account you touched.
Use this workflow only for X accounts you own or are authorized to manage. Treat every account cookie like a password.
Batch Plan
Treat a profile update as a controlled maintenance run, not a blind loop over accounts. The API call is simple: send POST https://api.twexapi.io/twitter/profile with your TwexAPI Bearer Token, the target account cookie, and only the fields this run is allowed to change.
A practical batch has four boundaries:
- Update one low-risk account first and inspect the live X profile before touching the full list.
- Keep one local configuration entry per account, with
updateslimited to the fields you intend to change. - Run a small worker pool, usually 2 to 3 workers, so failures stay easy to trace.
- Save a result file with success, failure, retry count, and error message for every account.
There is no separate preview endpoint for a profile change. Your practical dry run is a configuration review plus one low-risk account update, followed by a manual check of the live profile.
API Endpoint
POST https://api.twexapi.io/twitter/profile
Authorization: Bearer <your_twexapi_token>
Content-Type: application/jsonThe request body supports these fields:
| Field | Required | Purpose |
|---|---|---|
cookie | Yes | Authenticated cookie string for the X account being updated |
name | No | Display name |
website | No | Profile website URL |
description | No | Profile bio |
location | No | Profile location |
profile_image | No | Avatar image URL |
profile_banner | No | Banner image URL |
proxy | See note | Proxy URL used for this request |
The endpoint schema marks proxy as nullable, while the field description asks for a residential proxy. Do not treat it as a throwaway parameter in a batch job. Unless TwexAPI support has confirmed your integration can omit it, assign a stable proxy per account. If one account fails, this makes it easier to tell whether the issue is credentials, proxy quality, image URL access, or request fields.
Successful updates return:
{
"code": 200,
"msg": "success",
"data": true
}Update One X Account First
Start with a low-risk account and run the full request once. Set your TwexAPI token in the environment instead of placing it in source code:
export TWEXAPI_TOKEN="your_twexapi_bearer_token"Then send only the fields you want to change:
1curl --request POST \
2 --url https://api.twexapi.io/twitter/profile \
3 --header "Authorization: Bearer $TWEXAPI_TOKEN" \
4 --header "Content-Type: application/json" \
5 --data '{
6 "cookie": "auth_token=...; ct0=...; twid=...",
7 "proxy": "http://username:password@proxy.example.com:8000",
8 "name": "Acme Support",
9 "description": "Product help, release notes, and service updates.",
10 "location": "New York",
11 "website": "https://example.com/support",
12 "profile_image": "https://example.com/assets/support-avatar.png",
13 "profile_banner": "https://example.com/assets/support-banner.png"
14 }'This first request confirms three things: the cookie is still valid, the proxy can make the request, and the image URLs are publicly reachable. Move to the batch script only after the single-account update succeeds.
Only include values that should change. For example, omit profile_image and profile_banner when refreshing text fields only. Batch jobs are unforgiving: if an old value is present in your config, the script will write that old value back.
Prepare a Batch Configuration
Create a local profiles.json file. Each object represents one account, and updates should contain only the fields you are changing in this run. The account_id value is only a local label for the report. It is not sent to the API.
1[
2 {
3 "account_id": "acme-support-us",
4 "cookie": "auth_token=...; ct0=...; twid=...",
5 "proxy": "http://username:password@us-proxy.example.com:8000",
6 "updates": {
7 "name": "Acme Support US",
8 "description": "Product help and service updates for US customers.",
9 "location": "United States",
10 "website": "https://example.com/us/support",
11 "profile_image": "https://example.com/assets/us-avatar.png",
12 "profile_banner": "https://example.com/assets/us-banner.png"
13 }
14 },
15 {
16 "account_id": "acme-support-jp",
17 "cookie": "auth_token=...; ct0=...; twid=...",
18 "proxy": "http://username:password@jp-proxy.example.com:8000",
19 "updates": {
20 "name": "Acme Support JP",
21 "description": "Product help and service updates for customers in Japan.",
22 "location": "Japan",
23 "website": "https://example.com/jp/support"
24 }
25 }
26]Keep profiles.json outside version control because it contains account credentials. If the file must live inside a local project directory, add it to .gitignore and restrict its permissions:
chmod 600 profiles.jsonThe batch scripts below send only keys explicitly present in updates. Omitting a field leaves the existing profile value alone. Use an explicit null only when you intentionally want to clear a field and have tested that behavior with a single account first.
Python Batch Script
The script below validates each account, rejects unsupported fields, retries transient failures up to 3 times, and writes one result per account to profile-update-results.json. Install requests if your environment does not already include it:
python -m pip install requestsSave the following script as batch-update-profiles.py:
1import json
2import os
3import time
4from concurrent.futures import ThreadPoolExecutor, as_completed
5
6import requests
7
8API_URL = "https://api.twexapi.io/twitter/profile"
9ALLOWED_FIELDS = {
10 "name",
11 "website",
12 "description",
13 "location",
14 "profile_image",
15 "profile_banner",
16}
17MAX_ATTEMPTS = 3
18MAX_WORKERS = 3
19
20token = os.environ.get("TWEXAPI_TOKEN")
21if not token:
22 raise RuntimeError("Missing TWEXAPI_TOKEN environment variable.")
23
24def build_body(account):
25 account_id = account.get("account_id", "unnamed-account")
26 cookie = account.get("cookie")
27 updates = account.get("updates")
28
29 if not cookie:
30 raise ValueError(f"{account_id}: missing cookie")
31 if not isinstance(updates, dict) or not updates:
32 raise ValueError(f"{account_id}: updates must be a non-empty object")
33
34 unknown_fields = sorted(set(updates) - ALLOWED_FIELDS)
35 if unknown_fields:
36 raise ValueError(f"{account_id}: unsupported fields: {unknown_fields}")
37
38 body = {"cookie": cookie, **updates}
39 if account.get("proxy"):
40 body["proxy"] = account["proxy"]
41 return body
42
43def update_profile(account):
44 account_id = account.get("account_id", "unnamed-account")
45
46 try:
47 body = build_body(account)
48 except ValueError as error:
49 return {"account_id": account_id, "ok": False, "error": str(error)}
50
51 for attempt in range(1, MAX_ATTEMPTS + 1):
52 try:
53 response = requests.post(
54 API_URL,
55 headers={"Authorization": f"Bearer {token}"},
56 json=body,
57 timeout=45,
58 )
59 payload = response.json()
60
61 if response.ok and payload.get("code") == 200 and payload.get("data") is True:
62 return {"account_id": account_id, "ok": True, "attempts": attempt}
63
64 message = str(payload.get("msg", f"HTTP {response.status_code}"))
65 retryable = response.status_code == 429 or response.status_code >= 500
66 if not retryable:
67 return {"account_id": account_id, "ok": False, "error": message}
68 except (requests.RequestException, ValueError) as error:
69 message = f"{type(error).__name__}: request failed"
70
71 if attempt < MAX_ATTEMPTS:
72 time.sleep(attempt * 2)
73
74 return {
75 "account_id": account_id,
76 "ok": False,
77 "error": message,
78 "attempts": MAX_ATTEMPTS,
79 }
80
81with open("profiles.json", encoding="utf-8") as source:
82 accounts = json.load(source)
83
84if not isinstance(accounts, list) or not accounts:
85 raise RuntimeError("profiles.json must contain a non-empty array.")
86
87results = []
88with ThreadPoolExecutor(max_workers=min(MAX_WORKERS, len(accounts))) as executor:
89 futures = [executor.submit(update_profile, account) for account in accounts]
90 for future in as_completed(futures):
91 result = future.result()
92 results.append(result)
93 print(f"{result['account_id']}: {'updated' if result['ok'] else 'failed'}")
94
95with open("profile-update-results.json", "w", encoding="utf-8") as output:
96 json.dump(results, output, ensure_ascii=False, indent=2)
97
98success_count = sum(1 for result in results if result["ok"])
99print(f"Finished: {success_count}/{len(results)} profiles updated.")Run it with:
TWEXAPI_TOKEN="your_twexapi_bearer_token" python batch-update-profiles.pyFor the first run, keep only 1 or 2 accounts in profiles.json. After you confirm the result file, the live X profiles, and image loading, put the full list back.
Node.js Batch Script
If your team prefers Node.js, use this version. Node.js 18 and later include fetch, so it does not require an extra package. Save the script as batch-update-profiles.mjs:
1import { readFile, writeFile } from "node:fs/promises";
2
3const API_URL = "https://api.twexapi.io/twitter/profile";
4const ALLOWED_FIELDS = new Set([
5 "name",
6 "website",
7 "description",
8 "location",
9 "profile_image",
10 "profile_banner",
11]);
12const MAX_ATTEMPTS = 3;
13const MAX_WORKERS = 3;
14const token = process.env.TWEXAPI_TOKEN;
15
16if (!token) {
17 throw new Error("Missing TWEXAPI_TOKEN environment variable.");
18}
19
20const sleep = (milliseconds) =>
21 new Promise((resolve) => setTimeout(resolve, milliseconds));
22
23function buildBody(account) {
24 const accountId = account.account_id || "unnamed-account";
25 const updates = account.updates;
26
27 if (!account.cookie) {
28 throw new Error(`${accountId}: missing cookie`);
29 }
30 if (!updates || typeof updates !== "object" || Array.isArray(updates)) {
31 throw new Error(`${accountId}: updates must be a non-empty object`);
32 }
33
34 const updateKeys = Object.keys(updates);
35 if (updateKeys.length === 0) {
36 throw new Error(`${accountId}: updates must be a non-empty object`);
37 }
38
39 const unknownFields = updateKeys.filter((key) => !ALLOWED_FIELDS.has(key));
40 if (unknownFields.length > 0) {
41 throw new Error(`${accountId}: unsupported fields: ${unknownFields.join(", ")}`);
42 }
43
44 return {
45 cookie: account.cookie,
46 ...(account.proxy ? { proxy: account.proxy } : {}),
47 ...updates,
48 };
49}
50
51async function updateProfile(account) {
52 const accountId = account.account_id || "unnamed-account";
53 let body;
54
55 try {
56 body = buildBody(account);
57 } catch (error) {
58 return { account_id: accountId, ok: false, error: error.message };
59 }
60
61 let message = "request failed";
62 for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
63 try {
64 const response = await fetch(API_URL, {
65 method: "POST",
66 headers: {
67 Authorization: `Bearer ${token}`,
68 "Content-Type": "application/json",
69 },
70 body: JSON.stringify(body),
71 signal: AbortSignal.timeout(45_000),
72 });
73 const payload = await response.json().catch(() => ({}));
74
75 if (response.ok && payload.code === 200 && payload.data === true) {
76 return { account_id: accountId, ok: true, attempts: attempt };
77 }
78
79 message = String(payload.msg || `HTTP ${response.status}`);
80 const retryable = response.status === 429 || response.status >= 500;
81 if (!retryable) {
82 return { account_id: accountId, ok: false, error: message };
83 }
84 } catch (error) {
85 message = `${error.name}: request failed`;
86 }
87
88 if (attempt < MAX_ATTEMPTS) {
89 await sleep(attempt * 2_000);
90 }
91 }
92
93 return { account_id: accountId, ok: false, error: message, attempts: MAX_ATTEMPTS };
94}
95
96async function runQueue(accounts, workerCount) {
97 const results = new Array(accounts.length);
98 let cursor = 0;
99
100 async function worker() {
101 while (cursor < accounts.length) {
102 const index = cursor;
103 cursor += 1;
104 results[index] = await updateProfile(accounts[index]);
105 console.log(`${results[index].account_id}: ${results[index].ok ? "updated" : "failed"}`);
106 }
107 }
108
109 await Promise.all(
110 Array.from({ length: Math.min(workerCount, accounts.length) }, () => worker()),
111 );
112 return results;
113}
114
115const accounts = JSON.parse(await readFile("profiles.json", "utf8"));
116if (!Array.isArray(accounts) || accounts.length === 0) {
117 throw new Error("profiles.json must contain a non-empty array.");
118}
119
120const results = await runQueue(accounts, MAX_WORKERS);
121await writeFile("profile-update-results.json", JSON.stringify(results, null, 2));
122
123const successCount = results.filter((result) => result.ok).length;
124console.log(`Finished: ${successCount}/${results.length} profiles updated.`);Run it with:
TWEXAPI_TOKEN="your_twexapi_bearer_token" node batch-update-profiles.mjsBefore Running in Production
- Use the workflow only for accounts you are authorized to manage.
- Store
TWEXAPI_TOKENin an environment variable or secret manager. - Store X cookies and authenticated proxy URLs outside source control.
- Never print cookies or proxy credentials in logs or reports.
- Start with one account, then test a small batch before scaling up.
- Keep concurrency modest. Increasing worker count too quickly can make failures harder to diagnose.
- Retry only transient network failures, HTTP
429, and HTTP5xxresponses. Fix field errors and credential errors before running the job again. - Review
profile-update-results.jsonafter each run and retain it as an audit record. - Use stable public image URLs for
profile_imageandprofile_banner.