Upscale returns base64: how to get a CDN URL instead of a blob

Infrai's image upscale route always answers with base64 and a null url. Here is the two-call pattern that turns that blob into a signed, CDN-frontable link.

Not from that route, no. POST /v1/ai/image/upscale on Infrai always answers with data.image.b64_json, and the url and image_id fields sitting right next to it come back null — we tried store: true and they stayed null. The blob is the contract. What you can do, on the same key and without a second vendor, is hand the bytes straight to object storage and mint a signed URL your CDN can sit in front of.

That’s two extra calls and about fifteen lines of code, so the rest of this page is the exact shape of them, plus the places we found the documented behaviour and the live behaviour disagreeing.

What upscale actually returns

The input is base64, a data: URI, or an image id — not a URL. Point it at https://example.com/photo.png and you get a 400 back saying URL refs aren’t fetched in-process, which is a deliberate choice rather than an oversight: the service never makes an outbound fetch on your behalf.

API="https://api.infrai.cc"
export INFRAI_API_KEY="your_infrai_api_key"

base64 -i hero.png | tr -d '\n' > hero.b64
printf '{"image":"%s","scale":2}' "$(cat hero.b64)" > upscale.json

curl -sS "${API}/v1/ai/image/upscale" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d @upscale.json
{
  "ok": true,
  "data": {
    "image": {
      "b64_json": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2j…",
      "width": 32,
      "height": 32,
      "mime_type": "image/png",
      "image_id": null,
      "url": null
    },
    "original_size": "16x16",
    "new_size": "32x32"
  },
  "metadata": { "cost_usd": 0.01, "vendor": "infrai", "latency_ms": 312 }
}

original_size and new_size are handy for a sanity assertion. image_id and url are reserved fields that nothing populates today — worth flagging, because their presence in the response makes it look like a flag somewhere would fill them in. None does.

Base64 costs you roughly 33% more bytes on the wire than the raw file. On a 2048 × 2048 PNG that’s the difference between a 6 MB response and an 8 MB one, on every single request, both in and out of your process memory.

The storage hop

Object storage takes base64 directly, so you don’t even decode it — the string that came out of upscale goes straight into a data field. Write the payload to a file first; a multi-megabyte base64 string does not belong in a shell argument.

{
  "data": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2j…",
  "content_type": "image/png"
}
curl -sS -X PUT \
  "https://api.infrai.cc/v1/storage/object/put/kh-ai-upscale/upscaled/hero-2048.png" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d @stored.json

The key is everything after the bucket name, slashes included, so upscaled/hero-2048.png is one path parameter and prefixes work the way they do in S3. The response gives you bucket_id, key, size_bytes and an etag you can compare against a later HEAD.

Then sign it:

curl -sS -X POST \
  "https://api.infrai.cc/v1/storage/object/presign/kh-ai-upscale/upscaled/hero-2048.png" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"op":"download","expires_seconds":86400}'
{
  "ok": true,
  "data": {
    "url": "https://infrai-1333115350.cos.ap-singapore.myqcloud.com/…/upscaled/hero-2048.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=86400&X-Amz-Signature=412be…",
    "expires_at": "2026-07-27T02:19:31Z"
  }
}

That URL is a plain signed GET against the underlying object store. We fetched one and it returned the object with a 200, which is exactly what a CDN origin pull needs. Set expires_seconds to match your CDN’s cache TTL, or set the object’s ACL to public and point the CDN at the stable path instead — signed URLs and long CDN lifetimes fight each other.

One caveat we hit: op: "upload" presigned URLs came back with a signature the object store then rejected (SignatureDoesNotMatch). Download signing works; browser-direct upload didn’t, on the day we tested. If your plan was to have the browser PUT straight to storage, verify that path yourself before designing around it.

End to end in Node 22

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

const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("set INFRAI_API_KEY before running this");

const BASE = "https://api.infrai.cc";
const BUCKET = "kh-ai-upscale";
const OBJECT_KEY = "upscaled/hero-2048.png";
const auth = { Authorization: `Bearer ${key}`, "Content-Type": "application/json" };

const source = (await readFile("hero.png")).toString("base64");

const upscaleEndpoint = new URL("/v1/ai/image/upscale", BASE);
const upscaled = await fetch(upscaleEndpoint, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({ image: source, scale: 2 }),
}).then((r) => r.json());

if (!upscaled.ok) throw new Error(`upscale failed: ${upscaled.error?.code} ${upscaled.error?.message}`);
console.log(`${upscaled.data.original_size} -> ${upscaled.data.new_size}`);

const putBody = JSON.stringify({ data: upscaled.data.image.b64_json, content_type: "image/png" });
const stored = await fetch(`${BASE}/v1/storage/object/put/${BUCKET}/${OBJECT_KEY}`, {
  method: "PUT",
  headers: auth,
  body: putBody,
}).then((r) => r.json());

if (!stored.ok) throw new Error(`store failed: ${stored.error?.code}`);

const signed = await fetch(`${BASE}/v1/storage/object/presign/${BUCKET}/${OBJECT_KEY}`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({ op: "download", expires_seconds: 86400 }),
}).then((r) => r.json());

console.log({ bytes: stored.data.size_bytes, etag: stored.data.etag, cdn_origin: signed.data.url });

Verify it landed without downloading anything:

curl -sS "https://api.infrai.cc/v1/storage/object/list/kh-ai-upscale" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

Doesn’t the image module do this already?

Partly, and it’s worth knowing where it stops. POST /v1/image/process takes an image ref, an ops array and a format, and it does return a url field with store: true — but that url is a data: URI, the same bytes in a different wrapper. It also mints a real image_id (pim_…), which is a useful handle for chained transforms. What it does not give you is an https link, and the pim_ id it returns is not accepted as the image input to ai.image.upscale; we tried, and got a 400. The two modules don’t share a handle space yet.

RouteGives you a URL?Use it for
POST /v1/ai/image/upscaleNo — base64 onlyAI super-resolution, 2x and up
POST /v1/image/processA data: URI, plus an image_idDeterministic resize, crop, format conversion
PUT /v1/storage/object/put/{bucket}/{key}Yes, once signedDurable bytes your CDN can pull
POST /v1/storage/object/presign/{bucket}/{key}Yes, time-limitedHanding a link to a browser or CDN

Cost, and when to use something else

Upscaling is billed per image — the discovery manifest quotes about $0.01, and our test calls settled at exactly that, verified 26 July 2026. Storage adds a GB-month rent plus egress, which for a few thousand hero images is rounding error next to the inference. New accounts start with $2 of free credit, enough for roughly 199 upscales. Rates on this platform move downward over time, so read GET /v1/discovery for the number of the day rather than trusting this paragraph in six months.

If images are your whole product — signed transformation URLs, on-the-fly variants, format negotiation at the edge — a specialist like Cloudinary or Runware will do that better than a general platform, and you should use one. It’s fair to note that base64-only responses are the norm rather than the exception at this layer: OpenAI’s images API returns b64_json unless you ask for a hosted URL, and Amazon Bedrock’s image models hand back base64 too. The argument for doing it here is narrower. The key that upscaled the image also owns the bucket, so there’s no cross-account IAM to write, no second invoice, and GET /v1/account/usage shows ai.image.upscale and storage.object.put as two rows of the same bill.

References

Browse more ai developer guides