Bulk Text on Image Generator avatar

Bulk Text on Image Generator

Pricing

from $8.50 / 1,000 rendered images

Go to Apify Store
Bulk Text on Image Generator

Bulk Text on Image Generator

Add headlines and captions to batches of photos — text is automatically placed inside Instagram, TikTok, YouTube Shorts and Facebook's own safe zones, clear of where each platform's UI sits.

Pricing

from $8.50 / 1,000 rendered images

Rating

0.0

(0)

Developer

SIÁN OÜ

SIÁN OÜ

Maintained by Community

Actor stats

1

Bookmarked

2

Total users

1

Monthly active users

18 days ago

Last modified

Share

Bulk Text on Image Generator — Safe-Zone Ad Creative at Scale 🖼️

Store SIÁN Agency Store Print on Demand Designs Store Instagram Account 360 Store TikTok AI Transcript Extractor

🎉 Headlines rendered inside Instagram, TikTok, YouTube Shorts and Facebook's own safe zones — clear of where each platform's interface sits

Built for marketers and agencies turning one photo shoot into a week of platform-ready ad creative


🔎 What is the Bulk Text on Image Generator — and when should you use it?

The Bulk Text on Image Generator turns a batch of image URLs and headline text into clean, structured rows you can filter, export and feed straight into a spreadsheet, database or AI agent. No account, no portal API key, no browser automation to maintain.

Use it when you need: ready-to-post PNGs with the headline rendered inside each platform's safe zone, plus the exact pixel rectangle used, the font and size chosen, and a clear error code on any row that could not be rendered.

Use something else when: you need to generate the photo itself, not add a headline to one you already have. This Actor only adds a text layer to an existing image — it does not generate artwork, remove backgrounds or retouch the picture underneath.

🤖 Use with AI agents

Already connected to the Apify MCP server? Just ask for this Actor by name: sian.agency/bulk-text-on-image-generator

Your agent can pay for its own runs. This Actor is eligible for agentic payments, so an agent can discover it, run it and settle the bill over x402 (USDC on Base) or Skyfire — without an Apify account or API token of its own. Billing is the same either way: per successful row, never for errors.

Otherwise copy this prompt into Claude, ChatGPT, Cursor or any MCP-enabled assistant:

I want a batch of photos with an on-brand headline rendered on each one, safely inside every platform's UI-free zone using the Apify Actor `sian.agency/bulk-text-on-image-generator`.
Use it when I need: ready-to-post PNGs with the headline rendered inside each platform's safe zone, plus the exact pixel rectangle used, the font and size chosen, and a clear error code on any row that could not be rendered.
Don't use it when: This Actor only adds a text layer to an existing image — it does not generate artwork, remove backgrounds or retouch the picture underneath.
How to call it: supply one `imageUrl` + `text` pair for a single image, an array of `rows` for a handful, or a CSV for a full batch. A CSV can be uploaded from your machine or linked in `bulkImageUrls`. Pick a `preset` for the platform canvas and safe zone, then set `placement`, `textStyle` and `font` for how the headline looks.
Start with this input:
{
"bulkImageUrls": [
{
"requestsFromUrl": "https://example.com/images.csv"
}
],
"preset": "instagram-story-reels",
"placement": "bottom",
"textStyle": "none",
"font": "Anton"
}
Ask me which platform the images are going to, and roughly where they want the headline (top, center or bottom), then run the Actor and summarise the results as a table.

Things you can ask your agent for:

  • Take this CSV of 40 product photos and headlines and render them all as Instagram Story graphics.
  • Turn this one photo into feed, story and TikTok versions with the same headline.
  • Render these ad headlines with the band style so they stay legible over a busy background.

Machine-readable API, MCP config and OpenAPI definition for this Actor are published at apify.com/sian.agency/bulk-text-on-image-generator.md.

📋 Overview

Instagram, TikTok, YouTube Shorts and Facebook all draw their own interface on top of your image — a profile photo, a caption bar, an engagement rail — and a headline centered on the raw canvas has no idea any of that exists. This Actor renders your text inside each platform's own safe area instead, so a batch of product photos comes back ready to post rather than needing a second look at every single one.

What you get:

  • Platform-safe by construction: every placement is anchored inside that platform's safe area, never the raw canvas — clear of where that platform's interface sits.
  • 🎯 Seven ready-made canvases: Instagram Feed (4:5 and 1:1), Instagram Story/Reels, TikTok, YouTube Shorts, and Facebook Feed and Story — pick one and the crop, safe zone and text box all follow.
  • 📋 CSV in, ready-to-post PNGs out: upload a spreadsheet of image URLs and headlines straight from your computer, or link one by URL. Any column you skip inherits your run-wide defaults.
  • 🖌️ Five text styles, 31 fonts: plain, outline, band, scrim or drop-shadow rendering, and a font dropdown where every family has been checked to resolve, five of them bundled and instant.
  • 💰 Failures cost nothing: a row is only billed once its PNG is generated and saved — a broken image link or an unsupported font is never charged.
  • 🆓 Try it on 5 images for free: FREE renders at the same quality as PAID, just capped at five images per run so you can see real output first.

✨ Features

  • 🧭 Safe-zone anchoring: top, center and bottom are all relative to the platform's safe area, so the guarantee holds regardless of which one you pick.
  • 🎯 Seven platform presets: exact canvas and safe zone for Instagram Feed 4:5, Instagram Square 1:1, Instagram Story/Reels, TikTok, YouTube Shorts, Facebook Feed and Facebook Story.
  • 🖼️ Keep-original mode: skip the crop entirely and render onto the source's own dimensions, with the safe zone scaled proportionally.
  • ✂️ Cover or contain fitting: crop the source to fill the canvas edge-to-edge, or shrink it whole and pad the empty space with a color you choose.
  • 🖌️ Five text styles: none (plain text), outline, band, scrim and drop-shadow, each suited to a different kind of background — see below for which to trust blind.
  • 🔤 31-font dropdown, plus anything else on Google Fonts: pick from a checked list where five are bundled and instant, or name any other family and it resolves at render time.
  • 📏 Auto-shrink to fit: text shrinks until it fits your safe box, and a row that still can't fit fails cleanly instead of spilling outside it.
  • 🔁 Per-row CSV overrides: set your look once for the whole run, then override any of it — platform, placement, style, font, colors — one row at a time.
  • 🧮 Full render metadata on every row: the exact safe-zone rectangle, font and size used, line count, and whether the source was an animated file.

🧭 How the safe zone works — and why "top" isn't the top edge

On a 9:16 canvas — Instagram Story, Instagram Reels, TikTok or Facebook Story — placement: top puts your headline roughly 14% of the way down the image, not flush against the top edge. That's the guarantee working correctly: "top" means the top of the safe area, once the platform's own interface is excluded, not the top of the raw canvas. Without knowing that, the first time you see it, it reads as a bug.

The exact offset follows the platform. Instagram and Facebook's Story/Reels canvases start their safe area about 14% down; TikTok sits at the same 14%. YouTube Shorts reserves the most room, closer to 20%, because its own published reference marks a larger clear zone up top. Formats with no interface to clear work differently: on Instagram's 1:1 Square post, the top margin drops under 10%, so top there sits close to the literal top edge.

The same logic holds for bottom, center, and every side of every preset: the text box this Actor draws is always fully inside the safe rectangle, which is always fully inside the canvas. That containment is the one guarantee this Actor exists to keep, on every row, every time.

📏 Where the safe-zone numbers actually come from

None of the four platforms publishes its safe zone as a number you can point to and cite. Meta describes it in prose and shows it as an overlay inside Ads Manager, with no pixels anywhere in the text. TikTok ships it as a downloadable template file and says outright that the zone shifts with your caption length. YouTube draws its safe area onto a reference image instead of writing it down. So every inset built into this Actor's seven presets is the largest figure found across that platform's own published guidance and the industry references that fill the gap it leaves. Every one of them traces back to a public source.

That's not a compromise; it's the safer default on purpose. Where two sources disagree, the larger number always wins, so getting one wrong costs you a slightly smaller text box — never a non-compliant one.

🖌️ Which text style to trust on a photo you haven't looked at

  • Outline (default) — solid text with a contrasting stroke around every letter. Legible on any photo, because the stroke doesn't depend on what's behind it. The safest pick for a batch you're rendering sight-unseen.
  • Band — an opaque color block sits behind the text. Just as reliable as outline, for when you want a strong graphic block instead of a stroke.
  • Scrim — a soft gradient fade behind the text. Reads well on most photos, but the fade thins out toward its own edges, so treat it as a look rather than a legibility guarantee.
  • Shadow — an offset drop-shadow beneath the text, with nothing solid behind either one. This is the style that can lose contrast on a very bright or high-key photo. Pale text and a soft shadow can both wash out against a light background, so reach for outline or band instead on any batch you haven't reviewed by eye first.

🎬 Quick Start

Give it one image and a headline, a short list of rows, or a CSV link for a full batch. Pick a platform preset and the crop, safe zone and text box are handled for you.

curl -X POST https://api.apify.com/v2/acts/sian.agency~bulk-text-on-image-generator/runs?token=YOUR_TOKEN \
-H 'Content-Type: application/json' \
-d '{"imageUrl": "https://example.com/photo.jpg", "text": "SUMMER SALE", "preset": "instagram-story-reels"}'

🚀 Getting Started (3 Simple Steps)

Step 1: Give it your images

One imageUrl + text pair for a single graphic; a list of image URLs in Bulk Image URLs when they all share one headline; or a CSV when each image needs its own text — uploaded from your computer, or linked in Bulk Images. Priority runs uploaded CSV → rowsbulkImageUrls → single image, and each mode is used only when the ones before it are empty.

Step 2: Pick a platform preset

preset sets the output canvas and that platform's own safe zone. Leave outputMode on "Keep original size" instead if you don't want a crop at all.

Step 3: Choose placement and style

placement (top/center/bottom), textStyle (none/outline/band/scrim/shadow) and font decide how the headline looks — set them once for the whole run, or override any of them per row.

That's it. In a couple of minutes you'll have:

  • Ready-to-post PNGs, one per row, safe-zone compliant on every platform preset
  • A dataset row for every input row, including any that failed and exactly why
  • The precise safe-zone rectangle, font and size used for each render

📥 Input Configuration

FieldTypeRequiredDescription
imageUrlstringNoSingle image URL — used only when the CSV fields and rows are all empty
textstringNoHeadline for the single-image field above; \n forces a line break
rowsarrayNo[{ imageUrl, text, ...overrides }] for a handful of images
bulkImageUrlsarrayNoThe main bulk input — Add / Bulk edit / Text file. A linked .txt is a URL list (shared headline); a linked .csv carries its own per-row text
csvFilefileNoUpload a CSV from a local file — highest priority. A CSV already at a URL goes in bulkImageUrls instead
presetstringNoinstagram-feed-4x5 (default), instagram-square-1x1, instagram-story-reels, tiktok, youtube-shorts, facebook-feed, facebook-story
outputModestringNopreset — crop to the chosen canvas (default), or keepOriginal — keep the source's own dimensions
fitModestringNocover — crop to fill (default), or contain — fit the whole image, padded
padColorstringNoHex with or without the # (#ffffff, ffffff, #fff) or a CSS named color (e.g. white) for the padding contain leaves (default #ffffff)
placementstringNotop, center or bottom — anchored to the safe area, not the raw canvas (default bottom)
textStylestringNonone (default — plain text), outline, band, scrim or shadow
fontstringNoPick from 31 checked families in the dropdown; the five bundled ones load instantly with no lookup — Anton (default), Bebas Neue, Archivo Black, Lato, PT Serif
customFontstringNoAny Google Fonts family not in the dropdown (e.g. Space Grotesk) — overrides font when set
fontFilefileNoUpload your own .ttf/.otf — overrides both font and customFont. No WOFF/WOFF2, no variable fonts
textColorstringNoHex with or without the #, or a CSS named color, for the text (default #ffffff)
accentColorstringNoHex with or without the #, or a CSS named color, for the stroke, background or shadow — meaning depends on textStyle (default #111111)
textCasestringNonone — as typed (default), or upper — UPPERCASE
maxTextHeightPctnumberNoLargest share of the safe zone's height the text may fill before auto-shrinking stops (default 30)
marginPctnumberNoEmpty space kept clear on every side of the text box, capped at 45 (default 5)
maxOutputEdgePxintegerNoLongest-edge cap when outputMode is keepOriginal (default 2048)

Example — single image:

{
"imageUrl": "https://example.com/photo.jpg",
"text": "SUMMER SALE\n50% OFF",
"preset": "instagram-story-reels",
"placement": "top"
}

Example — bulk rows, one overriding the platform:

{
"rows": [
{ "imageUrl": "https://example.com/photo1.jpg", "text": "SUMMER SALE" },
{ "imageUrl": "https://example.com/photo2.jpg", "text": "NEW ARRIVALS", "preset": "tiktok", "placement": "top" }
]
}

📂 CSV batches — the exact template

Upload the file directly, or link it in Bulk Images. Either way the columns are the same. Only imageUrl and text are required, so a two-column CSV is valid on its own. Every other column is optional and named identically to its matching setting above. An empty cell inherits that run-wide setting, and a value in the row always beats the run-wide default. Unrecognized columns are ignored with a warning rather than an error, so a spreadsheet with columns of your own still works unchanged.

imageUrl,text,placement,textStyle,font,textColor,accentColor
https://cdn.example.com/1.jpg,"SUMMER SALE",top,outline,,,
https://cdn.example.com/2.jpg,"50% OFF\nTODAY ONLY",bottom,band,Bebas Neue,#ffffff,#e11d48

The full list of overridable columns: preset, fitMode, padColor, placement, textStyle, font, textColor, accentColor, textCase, maxTextHeightPct, marginPct. \n inside a text cell forces a line break, exactly like the single-image field.


📤 Output

Every input row becomes exactly one dataset row — successes and failures alike — so the row count always matches what you sent in.

FieldTypeDescription
statusstringok or failed
imageUrl / textstringEchoed from the input
outputUrlstringDirect link to the rendered PNG — null on a failed row
outputWidth / outputHeight / outputBytesintegerDimensions and file size of the PNG
preset / placement / textStylestringThe settings actually used for this row, after any CSV override
fontUsed / fontSource / fontSizePxstring / integerThe resolved font, whether it was bundled or fetched from Google Fonts, and the size it shrank to
linesRenderedintegerHow many lines the text wrapped to
safeRectobjectThe exact pixel rectangle of the safe zone used for this render
safeRectApproximatedbooleantrue when outputMode was "Keep original size" and the safe zone was scaled proportionally rather than read from the preset directly
sourceWasAnimatedbooleantrue when the source had multiple frames (e.g. an animated GIF) — only the first frame is rendered
errorCode / errorstringMachine-readable code and human-readable detail — null on a successful row

Example — a successful row:

{
"imageUrl": "https://example.com/photo1.jpg",
"text": "SUMMER SALE",
"outputUrl": "https://api.apify.com/v2/key-value-stores/.../records/image-00001.png",
"outputWidth": 1080,
"outputHeight": 1920,
"outputBytes": 1842213,
"preset": "instagram-story-reels",
"placement": "bottom",
"textStyle": "outline",
"fontUsed": "Anton",
"fontSource": "bundled",
"fontSizePx": 96,
"linesRendered": 2,
"safeRect": { "left": 65, "top": 270, "width": 950, "height": 882 },
"safeRectApproximated": false,
"sourceWasAnimated": false,
"status": "ok",
"errorCode": null,
"error": null
}

Every error code:

CodeMeaning
invalid_urlimageUrl isn't a valid http(s) link
download_failedThe image couldn't be fetched — timeout, DNS failure or a non-2xx response
unsupported_formatSomething downloaded, but it isn't a readable image — most often an error page served with a 200, an expired link, or hotlink protection
image_too_largeOver 25 MB or 40 megapixels
text_emptytext was blank
font_unavailableThe named font doesn't exist, isn't on Google Fonts, or has no fixed bold weight
glyphs_unsupportedThe font can't render one or more characters in the text
text_does_not_fitThe text still doesn't fit the safe box at the smallest readable size
render_failedThe image failed to composite
store_failedThe finished PNG couldn't be saved
invalid_presetAn unrecognized preset value — most often a CSV override typo
invalid_optionAn unrecognized placement, textStyle, fitMode or textCase value (case-sensitive: Bottom is not bottom), or an unrecognized textColor, accentColor or padColor value — the message lists what's allowed

A row is only billed once its PNG exists in storage, so every code above describes a row that cost you nothing.


💼 Use Cases & Examples

1. A week of ad creative from one photo shoot

A marketer has ten product photos and needs Feed, Story and TikTok versions of each.

Input: the same ten rows, run once per preset Output: thirty platform-ready PNGs, each with the text inside that platform's own safe zone Use: post the same campaign everywhere without re-cropping anything by hand

2. Batch headlines from a content-calendar spreadsheet

An agency manages one spreadsheet of product photos and this week's headlines for five clients.

Input: the export linked in Bulk Images (or uploaded), with a client-specific font or accentColor set per row Output: one finished PNG per row Use: turn Monday's planning meeting straight into Tuesday's ready-to-post assets

3. A/B testing headlines on the same photo

A performance marketer wants to test three headline variants on one hero image.

Input: three rows sharing one imageUrl, each with different text Output: three PNGs to run as separate ad variants Use: find the winning headline without opening a design tool

4. Seasonal sale banners across every platform at once

A retailer needs "50% OFF" on the storefront photo before a flash sale goes live.

Input: one row per preset, same imageUrl and text Output: seven differently-cropped PNGs, each safe-zone compliant for its own platform Use: ship the whole sale kit in a single run

5. Product photos with unpredictable backgrounds

A seller's catalogue photos vary wildly — some plain, some cluttered.

Input: textStyle: "band" set once for the whole batch Output: legible headlines regardless of what's behind them Use: skip reviewing every source photo individually before choosing a style

6. A Reel cover that respects Instagram's own UI

A creator has a vertical photo and wants text near the top, clear of the profile row.

Input: preset: "instagram-story-reels", placement: "top" Output: a PNG with the headline anchored just past Instagram's own interface clearance Use: trust the placement instead of eyeballing where the profile photo ends

A CSV import has forty rows and two dead image links.

Input: the same CSV, unedited Output: thirty-eight PNGs, plus two failed rows carrying download_failed and the exact URL that didn't resolve Use: fix the two broken links and re-run, instead of guessing which rows are missing


🔗 Integration Examples

JavaScript/Node.js

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('sian.agency/bulk-text-on-image-generator').call({
bulkImageUrls: [{ requestsFromUrl: 'https://example.com/images.csv' }],
preset: 'instagram-story-reels'
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((r) => r.status === 'ok').map((r) => r.outputUrl));

Python

from apify_client import ApifyClient
client = ApifyClient('YOUR_TOKEN')
run = client.actor('sian.agency/bulk-text-on-image-generator').call(
run_input={'imageUrl': 'https://example.com/photo.jpg', 'text': 'SUMMER SALE'}
)
for item in client.dataset(run['defaultDatasetId']).iterate_items():
print(item['status'], item['outputUrl'])

cURL

curl -X POST 'https://api.apify.com/v2/acts/sian.agency~bulk-text-on-image-generator/runs?token=YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"bulkImageUrls": [{ "requestsFromUrl": "https://example.com/images.csv" }], "preset": "tiktok"}'

Automation Workflows (N8N / Zapier / Make)

  1. Trigger: a new row lands in a content-calendar sheet
  2. HTTP Request: call the Actor with that row's image URL and headline
  3. Wait: for the run to finish
  4. Action: pull outputUrl and post it, or save it to your asset library

📊 Performance & Pricing

FREE Tier (Try It Now)

  • 5 images per run, after exact duplicates are removed — full feature access, same render quality as PAID
  • No credit card required
  • Perfect for testing a preset and style before committing to a full batch
  • Unlimited images per run
  • Every preset, style and font available
  • Billed only once an image is rendered and saved — a failed row is free

💰 You only pay for images that exist. Validation runs before any charge, and the charge itself fires only after the PNG is saved to storage — a bad URL, an unsupported font, or text that can't fit costs you nothing.

🔗 View current pricing


❓ Frequently Asked Questions

Q: How many images can I process? A: FREE tier: 5 per run, after removing exact duplicates. PAID tier: unlimited.

Q: Why did placement: top land partway down the image instead of at the edge? A: placement is relative to the platform's safe area, not the raw canvas. On a 9:16 Story or Reel that safe area itself starts about 14% down, so top puts your text there — not at pixel zero. It's the guarantee working as intended: the text box stays inside the safe area, clear of where the platform's caption bar and profile ring typically sit.

Q: Are the safe-zone numbers official platform specifications? A: No — no platform publishes one. They're the largest figure found across each platform's own published guidance and the industry references that fill the gap it leaves, so a disagreement between sources costs you a slightly smaller text box, never a non-compliant image.

Q: Which text style should I use if I haven't looked at my source photos? A: outline or band — both guarantee contrast regardless of what's in the photo. shadow can lose contrast on a very bright or high-key image, because nothing solid sits behind the text.

Q: What happens to a row that fails? A: It's still written to the dataset with status: "failed" and an errorCode, so the output row count always matches what you sent in — and you're never charged for it.

Q: Can I use my own font? A: Yes. The Font dropdown offers 31 families that have each been checked to resolve to a real bold face, five of them bundled and instant. For anything else on Google Fonts, type the family name into Custom Font Family and it overrides the dropdown. That family is fetched at bold weight the first time a row uses it. And if the font is yours rather than Google's, upload the .ttf/.otf directly — that beats both. Web-font formats (.woff/.woff2) and variable fonts are rejected with a clear message: export a single static weight first.

Q: What image formats are supported? A: JPEG, PNG, WebP, GIF, TIFF and AVIF, up to 25 MB and 40 megapixels. An animated source renders its first frame only, and the row reports that it happened.

Q: What output formats are available? A: The rendered images are always PNG. The dataset itself exports as JSON, CSV or Excel.


🐛 Troubleshooting

A row failed with text_does_not_fit

  • The headline is too long for the safe box at the smallest readable size. Raise maxTextHeightPct, lower marginPct, or shorten the text.

A row failed with font_unavailable

  • The font name isn't on Google Fonts, or exists only as a variable font with no fixed bold weight. Try one of the five bundled fonts, or a different Google Fonts family.

A row failed with glyphs_unsupported

  • The chosen font can't render one or more characters in your text — common with emoji or non-Latin scripts on a narrow display font. Switch to a font family built for that script.

My CSV columns aren't being picked up

  • Column names must match exactly: imageUrl, text, and the override columns listed above. An unrecognized column is ignored with a warning, not an error — check the run log.

A CSV row's override didn't apply

  • An empty cell inherits the run-wide setting; only a non-empty cell overrides it. Check for stray whitespace in the cell.

Text looks washed out on one image

  • That row likely used textStyle: "shadow" over a bright photo. Switch it to outline or band.

🤝 Support

Telegram Support

Join our active support community


Built by SIÁN Agency | More Tools