Automatically Generate Tweet Images With an API
Build a reliable pipeline that generates an image, uploads it to X, creates a Post, and handles authentication, validation, retries, and rate limits.
The reliable way to automatically generate a tweet image is a two-stage pipeline:
- Generate or edit the artwork with an image-generation API.
- Upload the resulting bytes to X, then create the Post with the returned media ID.
Keep the image bytes, prompt, model, dimensions, format, upload response, and Post response in one job record. Upload first and publish second. The older combined statuses/update_with_media endpoint is deprecated; new integrations should use the upload-then-Post sequence.
Architecture
prompt + options
|
v
image generation API ----> image bytes (PNG/JPEG/WebP)
|
v
X media upload (user auth)
|
v
media_id
|
v
X Post creation
|
v
saved URL, IDs, and status
OpenAI documents two image-generation approaches: the Image API for a direct generation or edit, and the Responses API image-generation tool for a multi-step or conversational flow. Both expose controls such as size, quality, output format, compression, background, and action selection. See the OpenAI image-generation guide.
Choose the generation API
Image API
Use the Image API when one request should create or edit an image. It is a good fit for scheduled jobs, queue workers, and simple prompt-to-image commands. Store the returned image data immediately in the format you selected for publishing.
Responses API image-generation tool
Use the Responses API image-generation tool when image creation is one step in a larger conversation or workflow, such as selecting a concept, revising it, and then publishing the approved version. The same guide describes multi-step image flows and output controls.
Design the job before writing code
A production job should contain at least:
job_idand an idempotency key generated by your application.- The exact prompt, model, requested width and height, quality, background, and output format.
- The generated bytes or a durable object-storage reference.
- The X user and application identity used for the upload.
- The returned X media ID and Post ID.
- Attempt counts, timestamps, HTTP status codes, and the final error category.
Do not regenerate an image merely because the upload request timed out. First check whether the upload completed and whether a media ID was returned. Your own idempotency record is the safest way to prevent duplicate Posts; retry behavior and idempotency guarantees are not promised by the platform documentation.
Generate an image with the Image API
The exact model name, current pricing, quotas, and available dimensions change over time. Read the current image guide and substitute a model enabled for your account. The following Python example shows the request shape and preserves the returned bytes; adapt the response-field extraction to the SDK version you use.
import base64
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
result = client.images.generate(
model=os.environ.get("OPENAI_IMAGE_MODEL", "gpt-image-1"),
prompt=(
"A clear editorial illustration for a developer newsletter about API reliability, "
"high contrast, one central subject, leave safe margins for social cropping"
),
size="1536x1024",
quality="high",
output_format="png",
)
# Image APIs commonly return base64 image data. Preserve the bytes before upload.
image_bytes = base64.b64decode(result.data[0].b64_json)
with open("tweet-image.png", "wb") as file:
file.write(image_bytes)
Choose a canvas that matches the way your account presents images, then inspect the crop in your own publishing flow. The documented standard sizes include 1024×1024, 1536×1024, and 1024×1536, with custom dimensions available for supported models.
Edit an existing image
For an edit, retain the original asset, the mask or edit inputs, and the new prompt. This lets you reproduce a failed publication without asking the model for a different image.
from openai import OpenAI
client = OpenAI()
with open("source.png", "rb") as source:
result = client.images.edit(
model="gpt-image-1",
image=source,
prompt="Keep the composition, replace the background with a dark blue gradient, and leave clear space at the top for cropping.",
output_format="png",
)
# Persist the returned image bytes using the response format documented for your SDK version.
Upload the image to X
X requires user-context authentication for write actions. Upload one or more media entities first, then pass the resulting media identifiers when you call the Post endpoint. The deprecated combined endpoint should not be used for new code. Consult the X media upload documentation and the current X Posts documentation for the authentication scheme and request fields enabled for your account.
The upload API has historically been exposed as a v1.1 media endpoint while Post creation is part of the newer X API. Keep those calls separate in your code so a successful upload can be recorded and reused.
cURL request pattern
# Use the authentication method and endpoint shown in the current X documentation.
curl --request POST \
--url "$X_MEDIA_UPLOAD_URL" \
--header "Authorization: Bearer $X_USER_ACCESS_TOKEN" \
--form "media=@tweet-image.png"
# Then create the Post with the returned media identifier.
curl --request POST \
--url "$X_POST_URL" \
--header "Authorization: Bearer $X_USER_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{"text":"A generated image attached through the API","media":{"media_ids":["MEDIA_ID_FROM_UPLOAD"]}}'
Use the exact upload URL, token type, and JSON shape required by your X access level. Do not put bearer tokens in source control or client-side code.
Python requests example
import os
import requests
TOKEN = os.environ["X_USER_ACCESS_TOKEN"]
MEDIA_UPLOAD_URL = os.environ["X_MEDIA_UPLOAD_URL"]
POST_URL = os.environ["X_POST_URL"]
with open("tweet-image.png", "rb") as image_file:
upload = requests.post(
MEDIA_UPLOAD_URL,
headers={"Authorization": f"Bearer {TOKEN}"},
files={"media": ("tweet-image.png", image_file, "image/png")},
timeout=60,
)
upload.raise_for_status()
upload_body = upload.json()
media_id = upload_body.get("media_id_string") or upload_body.get("media_id")
if not media_id:
raise RuntimeError(f"Upload succeeded without a media ID: {upload_body}")
post = requests.post(
POST_URL,
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
},
json={
"text": "A generated image attached through the API",
"media": {"media_ids": [str(media_id)]},
},
timeout=60,
)
post.raise_for_status()
print(post.json())
Node.js example
import fs from "node:fs";
const token = process.env.X_USER_ACCESS_TOKEN;
const mediaUploadUrl = process.env.X_MEDIA_UPLOAD_URL;
const postUrl = process.env.X_POST_URL;
const form = new FormData();
form.append("media", new Blob([fs.readFileSync("tweet-image.png")], { type: "image/png" }), "tweet-image.png");
const uploadResponse = await fetch(mediaUploadUrl, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form,
});
if (!uploadResponse.ok) throw new Error(`Media upload failed: ${uploadResponse.status}`);
const uploadBody = await uploadResponse.json();
const mediaId = uploadBody.media_id_string ?? uploadBody.media_id;
if (!mediaId) throw new Error("The upload response did not contain a media ID");
const postResponse = await fetch(postUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "A generated image attached through the API",
media: { media_ids: [String(mediaId)] },
}),
});
if (!postResponse.ok) throw new Error(`Post creation failed: ${postResponse.status}`);
console.log(await postResponse.json());
Validate before publishing
- Confirm the file is non-empty and can be decoded by an image library.
- Check the MIME type and extension match the bytes.
- Use a supported canvas size and inspect the likely crop on mobile and desktop.
- Keep text away from edges so it survives responsive rendering.
- Reject an upload response that lacks a media identifier.
- Publish only after the upload has succeeded.
Retries, rate limits, and reliability
Retry transient network failures and 5xx responses with exponential backoff and jitter. A practical schedule is three attempts at roughly 1, 2, and 4 seconds, capped by your job deadline. Do not retry 401 authentication failures, permission errors, malformed requests, or media validation failures until the underlying configuration changes.
X documents 401 authentication errors and 429 rate-limit responses. Respect a server-provided reset time when available, queue work per account, and cap concurrency. Generated-image requests, uploads, and Posts can each fail independently, so record a state machine such as GENERATING, GENERATED, UPLOADED, PUBLISHED, and FAILED.
When a Post call times out, query your own job record and the account’s recent Posts before submitting again. This reduces duplicate publication. The retry strategy here is an engineering recommendation, not a platform guarantee.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 from X | Expired token, wrong token type, or missing user authorization | Refresh the user-context credentials and verify the app has write access. |
| 403 or permission error | The account or app access level does not allow the operation | Check the current X access plan and requested scopes. |
| 429 | Rate limit exceeded | Stop immediate retries, honor reset information, and drain a per-account queue. |
| Media validation failure | Unsupported bytes, MIME type, dimensions, or a damaged file | Decode the saved bytes locally, set the correct content type, and regenerate or convert the asset. |
| Upload works but Post fails | Wrong media ID field or invalid Post payload | Log the upload JSON, use the documented media identifier, and validate the Post body separately. |
| Duplicate Posts | A timeout was retried without checking the first result | Persist an idempotency key and reconcile status before retrying. |
| Image looks cropped | Canvas does not match the display context | Generate with a suitable aspect ratio, preserve safe margins, and preview the final flow. |
Performance and cost
Generation usually dominates latency and spend, while upload and Post creation add separate network requests. Store the generated bytes once, avoid regenerating during upload retries, and process scheduled work through a queue. Select output quality and dimensions that fit the visual requirement; larger or higher-quality outputs can increase transfer time and image-generation cost.
Current model availability, pricing, quotas, and X access rules are volatile. Verify them at implementation time in the official documentation. Set request timeouts, enforce a maximum job duration, and emit metrics for generation latency, upload latency, Post latency, retry count, and terminal error category.
Or skip the browser setup
If the image is a screenshot of a web page, dashboard, landing page, or generated preview, ScreenshotNeo provides a single screenshot API request instead of maintaining browser automation. The API and full option list are in the ScreenshotNeo docs.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server lets AI agents such as Claude and Cursor take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
FAQ
Can one API call generate and publish the image?
Treat generation, media upload, and Post creation as separate steps. That makes failures recoverable and follows X’s current upload-then-Post flow.
Which image format should I choose?
Choose a format accepted by the current X upload documentation and preserve the same bytes through upload. PNG is useful for sharp text; JPEG can reduce size for photographic artwork.
Can I schedule many generated Posts?
Yes. Queue jobs per account, cap concurrency, and account for both image-generation quotas and X rate limits.
Should prompts include text that must appear in the image?
Keep important copy in the Post text when possible. If text must be inside the artwork, generate with safe margins and inspect the actual published crop.


