Posters and social ads from an image API: resolution, style control, upscale

What high quality means for marketing creative, which parts of it an API can actually give you, and a render-then-upscale pipeline with the dimensions we measured.

“High quality” splits into three separate problems the moment you try to buy it: pixel dimensions that match the ad slot, control over look so a campaign hangs together, and legible typography inside the image. An API can solve the first cleanly, the second only partly, and the third — today — barely at all. Infrai’s image surface handles the first two and hands you a separate upscaling step for print-size output; the third is where you’ll want a specialist, and we’ll show you exactly where the line falls.

Everything below was measured against a live account on 2026-07-26, including the parts that came back worse than we expected.

Which models are worth pointing a campaign at

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS "https://api.infrai.cc/v1/ai/models?capability=image&available=true" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  | jq '[.data[] | {id, owned_by, unit, price_usd}]'
[
  { "id": "gpt-image-1.5", "owned_by": "azure_foundry", "unit": "per_token" },
  { "id": "gpt-image-2", "owned_by": "azure_foundry", "unit": "per_token" },
  { "id": "wan-t2i", "owned_by": "wanxiang", "unit": "per_image", "price_usd": 0.02 },
  { "id": "wanx-v1", "owned_by": "wanxiang" },
  { "id": "wanx2.1-t2i-turbo", "owned_by": "wanxiang", "unit": "per_image", "price_usd": 0.014 }
]

For marketing creative the split is simple. The gpt-image family is the stronger renderer and the one to reach for on hero assets — it’s also the one whose per-token billing makes cost per poster unpredictable. The wanx models are quick and cheap and fine for volume variants: background plates, seasonal recolours, the fifteenth crop of the same product shot.

Rates read that day were $0.014 and $0.02 per image, but treat those as a floor rather than a budget — our metered charge came in higher, and image rates in general keep falling, so re-read the catalogue before you plan a campaign around a number.

Resolution: the size field is not a preset list

This is the single most useful thing we learned, and it isn’t documented anywhere obvious. size is free-form, not an enum of three squares. We asked for a deliberately odd 999x999 and got back a PNG that file reports as exactly 999 × 999 pixels.

That means the ad slot drives the request, not the other way round.

const KEY = process.env.INFRAI_API_KEY;
const BASE = process.env.INFRAI_BASE_URL;
if (!KEY || !BASE) throw new Error("set INFRAI_API_KEY and INFRAI_BASE_URL");

const SLOTS = {
  instagram_portrait: "1080x1350",
  instagram_square: "1080x1080",
  display_leaderboard: "1200x628",
  story: "1080x1920",
};

export async function renderSlot(slot, prompt, model = "wanx2.1-t2i-turbo") {
  const size = SLOTS[slot];
  if (!size) throw new Error(`unknown slot ${slot}`);

  const res = await fetch(`${BASE}/v1/images/generations`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ model, prompt, n: 1, size }),
  });
  if (!res.ok) throw new Error(`render failed: ${res.status} ${await res.text()}`);

  const json = await res.json();
  return {
    url: json.data[0].url,
    size,
    cost: json.infrai?.cost_usd ?? null,
    model: json.infrai?.model ?? model,
  };
}

One caveat that follows directly from the freedom: nothing validates the string. Ask for a nonsense aspect ratio and you’ll get one, billed at the normal rate. Keep the slot table in code, as above, rather than letting a size reach the API from user input.

The returned URL is a signed link on the vendor’s bucket with an expiry measured in hours. For a campaign asset that’s not good enough — download the bytes and put them in storage you control on the same key, which is one PUT /v1/storage/object/put/{bucket}/{key} away.

Style control is prompt-only, and that has a real cost

Here’s the honest limitation. There’s no negative prompt, no seed, no style-reference image and no preset on this surface — the whole style API is the prompt string. That’s fine for one poster and awkward for a campaign, because consistency across twelve assets is normally bought with a fixed seed or a style reference.

The workaround that actually holds up is a style preamble you never edit, stored once and prefixed to every prompt:

{
  "style_preamble": "editorial product photography, soft north-facing window light, shallow depth of field, muted warm palette, matte finish, no text, no logos, no people",
  "brand_notes": "keep the product centred with 15% margin; background stays neutral grey"
}

Concatenate that in front of every campaign prompt and lock the file behind review. You’ll still get drift between renders — sometimes enough to reject an asset — but far less than freehand prompting, and it costs nothing. If deterministic re-rendering from a seed is a hard requirement, this isn’t the right tool and Stability’s platform is the one built for it.

Typography is the other gap, and it’s worth being blunt: none of the served models reliably renders words inside a poster. If your creative needs a headline baked into the pixels, Ideogram exists specifically because everyone else is bad at this. The alternative — and the approach most marketing tools actually ship — is to generate the plate without text and compose the headline in SVG or Canvas afterwards, where it’s editable, translatable and pixel-exact.

Upscaling is a separate, cheap, self-hosted step

Render at the model’s comfortable size, then enlarge. The upscale route runs on Infrai’s own infrastructure rather than a third-party vendor, which is why it answers in well under a second:

curl -sS -X POST "https://api.infrai.cc/v1/ai/image/upscale" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
    "factor": 4
  }'
{
  "ok": true,
  "data": {
    "image": { "b64_json": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAQ…", "width": 4, "height": 4, "mime_type": "image/png" },
    "original_size": "1x1",
    "new_size": "4x4"
  },
  "metadata": { "latency_ms": 82, "vendor": "infrai", "vendor_region": "western", "cost_usd": 0.01 }
}

Confirm that rate yourself rather than taking ours — the discovery manifest carries the current figure for every billable route:

curl -sS "https://api.infrai.cc/v1/discovery" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  | jq '[.capabilities[] | select(.id | startswith("ai.image")) | {id, price: .billing.price_usd, unit: .billing.unit}]'
[{ "id": "ai.image.upscale", "price": 0.01, "unit": "per_image" }]

Verified 2026-07-26: $0.01 per image, factor 2 or 4, 82 ms on a trivial input, and the result comes back as base64 rather than a URL unless you ask it to be stored. Rates here move down over time, so that call is the one to trust. So a 1080 × 1350 social render becomes a 4320 × 5400 print-ready plate for one extra cent — which reframes the whole quality question. You don’t need the model to emit huge images; you need it to emit good composition at a size it’s good at, and then you enlarge.

That pipeline in full:

import { writeFile } from "node:fs/promises";

const KEY = process.env.INFRAI_API_KEY;
const BASE = process.env.INFRAI_BASE_URL;
if (!KEY || !BASE) throw new Error("set INFRAI_API_KEY and INFRAI_BASE_URL");

export async function posterPipeline(prompt, { size = "1080x1350", factor = 4 } = {}) {
  const gen = await fetch(`${BASE}/v1/images/generations`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ model: "wanx2.1-t2i-turbo", prompt, n: 1, size }),
  });
  if (!gen.ok) throw new Error(`render failed: ${gen.status} ${await gen.text()}`);
  const { data, infrai } = await gen.json();

  const up = await fetch(`${BASE}/v1/ai/image/upscale`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ image: data[0].url, factor }),
  });
  if (!up.ok) throw new Error(`upscale failed: ${up.status} ${await up.text()}`);
  const upscaled = await up.json();

  const bytes = Buffer.from(upscaled.data.image.b64_json, "base64");
  await writeFile("poster.png", bytes);

  const spend = (infrai?.cost_usd ?? 0) + (upscaled.metadata?.cost_usd ?? 0);
  console.log(`${upscaled.data.original_size} -> ${upscaled.data.new_size}, $${spend.toFixed(4)}`);
  return { file: "poster.png", spend };
}

Check the routing modes available to you while you’re here — auto, cheapest and the pinned vendor/model forms all appear in the compat catalogue:

curl -sS "https://api.infrai.cc/v1/models" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

The comparison, with the rows that go against us

RequirementInfrai image surfaceIdeogramStabilityOpenAI direct
Arbitrary output dimensionsyes, free-form sizepreset aspect ratiospreset + custompreset sizes
Headline text inside the imageunreliablethe reason it existspartialpartial
Seed / deterministic re-rendernoyesyeslimited
Style reference imagenoyesyesvia edits
Upscale as its own callyes, self-hosted, sub-secondbundledyesno
Storage, queue, email on the same keyyesnonono
Contracted region for the renderno — routed to a china-region vendorvendor-definedvendor-definedyes via Azure OpenAI

Two of those seven rows are ours. That’s the fair reading, and it’s the same conclusion we’d give a friend: if creative control is the product, buy the specialist. If image generation is one feature inside a marketing app that also has to store assets, queue renders, email approvals and bill tenants, the arithmetic changes, because those four are already on the credential you’re holding.

Start with the free catalogue call, render one asset into your real ad slot, upscale it, and put it in front of whoever signs off the creative. That takes about ten minutes and settles the argument better than any table.

References

Browse more ai developer guides