Back to Home

Image Compose API

Combine 1–3 reference images with a text prompt to synthesize a new image. Common use cases: place a character into a different scene, compose multiple products in one frame, style-transfer composites, and so on.

Editing a single image? Use the Image Edits API — outfit swaps, scene replacements, variants.

Pure text-to-image? Use the Image Generation API.

When to use which endpoint

ScenarioEndpointProtocol
Pure text-to-image/v1/images/generationsJSON
Single-image edit/v1/images/editsJSON + image_key or generation_id
Multi-image compose/v1/images/composeJSON + image_keys[] / generation_ids[] / image_urls[] (1–3)

Request

POST /v1/images/compose

Headers

Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx
Content-Type: application/json

x-api-key (Anthropic SDK style) is also supported.

Body

ParameterTypeRequiredDescription
modelstringModel name, e.g. gpt-image-2
promptstringText instruction, up to 32000 characters
image_keysstring[]⚠️List of R2 keys obtained from the File Upload APIat least one of image_keys / generation_ids / image_urls is required
generation_idsstring[]⚠️List of UUIDs from previous /v1/images/{generations,edits,compose} responses — at least one of image_keys / generation_ids / image_urls is required
image_urlsstring[]⚠️Inline image list — each entry is either an https://... URL or a data:image/(jpeg|png|gif|webp);base64,<data> data URI. No upload step required. At least one of image_keys / generation_ids / image_urls is required
nintegerNumber of images, default 1, max determined by model config
sizestringImage size, same as Image Generation API

Reference image total: image_keys.length + generation_ids.length + image_urls.length must be between 1 and 3. All three arrays can be mixed. Duplicate references (including the same image across arrays) are rejected.

image_urls security constraints:

  • https:// URLs must use HTTPS. Tokensmart blocks private / loopback / link-local / CGN addresses, blocks redirects, blocks userinfo (user:pass@host), validates the response Content-Type is image/(jpeg\|png\|gif\|webp), enforces a 30-second timeout, and caps each fetched image at 25MB.
  • data: URIs must be a valid data:image/(jpeg\|png\|gif\|webp);base64,... string (≤35MB), decode to ≤25MB of raw bytes, and the file's magic bytes must match the declared MIME.
  • Inline images flow through Workers memory only — they are NOT written to R2, do NOT count toward your 200MB file quota, and do NOT appear in /api/images/history.

Full Example

Compose two previously-generated images:

{
  "model": "gpt-image-2",
  "prompt": "Place the person from image 1 into the scene from image 2, keep clothing, unify lighting",
  "generation_ids": [
    "00000000-0000-0000-0000-000000000001",
    "00000000-0000-0000-0000-000000000002"
  ],
  "n": 1
}

Mix uploaded image with generated image:

{
  "model": "gpt-image-2",
  "prompt": "Print this logo on the product, on a white wooden table background",
  "image_keys": ["files/<userId>/<key>.png"],
  "generation_ids": ["<uuid>"],
  "n": 1
}

Pass base64 inline images (no upload step):

{
  "model": "gpt-image-2",
  "prompt": "Place the person from image 1 into the scene from image 2, keep clothing, unify lighting",
  "image_urls": [
    "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...",
    "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
  ],
  "n": 1
}

Or pass public HTTPS URLs:

{
  "model": "gpt-image-2",
  "prompt": "Place the person into this scene",
  "image_urls": [
    "https://example.com/portrait.jpg",
    "https://example.com/scene.jpg"
  ],
  "n": 1
}

Response

{
  "created": 1776864985,
  "data": [
    {
      "b64_json": "iVBORw0KGgo...",
      "revised_prompt": "..."
    }
  ],
  "picklyone_generation_ids": ["00000000-0000-0000-0000-000000000099"]
}
FieldTypeDescription
data[i].b64_jsonstringbase64-encoded PNG (no data: prefix — decode directly with base64)
data[i].revised_promptstring | nullModel's rewritten prompt (debug-only, may be null)
picklyone_generation_ids[i]stringUUID of the generated image — can be passed as generation_id to a subsequent /v1/images/{compose,edits} call for chained composition

Billing

Per-image billing, identical to the Image Generation API: actualCount × price_per_image. Pre-deduction freezes balance for the requested n; settlement charges for actually returned images; the difference is auto-refunded.

Python Example

import requests, base64

API_KEY = "pk_live_xxxxxxxxxxxxxxxx"
BASE = "https://api.tokensmart.ai"

resp = requests.post(
    f"{BASE}/v1/images/compose",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "model": "gpt-image-2",
        "prompt": "Place the person from image 1 into the scene from image 2",
        "generation_ids": [
            "00000000-0000-0000-0000-000000000001",
            "00000000-0000-0000-0000-000000000002",
        ],
        "n": 1,
    },
    timeout=300,
).json()

for i, item in enumerate(resp.get("data", [])):
    if item.get("b64_json"):
        with open(f"out_{i}.png", "wb") as f:
            f.write(base64.b64decode(item["b64_json"]))
        print(f"saved out_{i}.png — generation_id: {resp['picklyone_generation_ids'][i]}")

cURL Example

curl https://api.tokensmart.ai/v1/images/compose \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-image-2",
    "prompt": "Place the person from image 1 into the scene from image 2",
    "generation_ids": [
      "00000000-0000-0000-0000-000000000001",
      "00000000-0000-0000-0000-000000000002"
    ]
  }'

Node.js Example

import fs from "fs";

const API_KEY = "pk_live_xxxxxxxxxxxxxxxx";
const BASE = "https://api.tokensmart.ai";

const resp = await fetch(`${BASE}/v1/images/compose`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "gpt-image-2",
    prompt: "...",
    image_keys: ["files/<userId>/<key>.png"],
    generation_ids: ["<uuid>"],
    n: 1,
  }),
}).then(r => r.json());

for (const [i, item] of resp.data.entries()) {
  if (item.b64_json) {
    fs.writeFileSync(`out_${i}.png`, Buffer.from(item.b64_json, "base64"));
  }
}

Limits

ItemLimit
Request body100 MB (sized for inline image_urls; if you only use image_keys / generation_ids, requests are far smaller)
Per reference image25 MB (same cap for R2-sourced and inline)
Single image_urls data URI string35 MB (decodes to ≈25 MB of raw bytes)
Single image_urls HTTPS URL2048 characters
Reference images combined30 MB (R2-sourced + inline)
Reference image count1–3 (image_keys + generation_ids + image_urls combined)
Images per requestDetermined by model's max_n_per_request (n above this returns 400 invalid_request)
Upstream timeoutTypically 30–90 seconds
Rate limit30 requests/min/user (shared with other image endpoints)

Error Codes

HTTPCodeDescription
400invalid_requestField format invalid / n out of range / size malformed
400invalid_sourceZero references, more than 3, or none of the three arrays provided
400invalid_image_keyimage_key malformed or doesn't belong to the caller
400invalid_generation_idgeneration_id is not a valid UUID
400invalid_urlimage_urls entry is not https:// or data:image/..., or URL exceeds 2048 chars
400invalid_data_uriimage_urls data URI is malformed (MIME not in allowlist or base64 payload missing)
400image_too_largeimage_urls data URI string exceeds 35MB, or decoded bytes exceed 25MB
400MAGIC_MISMATCHimage_urls actual file magic bytes do not match the declared MIME (anti-spoofing)
400IMAGE_TYPE_UNSUPPORTEDimage_urls remote response Content-Type is not in image/(jpeg|png|gif|webp) allowlist
400NON_HTTPS / BLOCKED_HOST / PRIVATE_IP / REDIRECT_BLOCKED / NOT_IMAGE / TOO_LARGE / UPSTREAM_ERRORimage_urls SSRF / fetch validation failed (see security constraints above)
400IMAGES_TOO_LARGE_TOTALSum of all three sources exceeds 30MB
400duplicate_referenceSame image referenced multiple times (within any single array, or across image_keys + generation_ids)
400content_policy_violationPrompt triggered content moderation (pre-deduction refunded)
400no_image_generatedModel returned zero images for a non-policy reason (pre-deduction refunded)
401invalid_api_keyInvalid API key
402insufficient_balanceInsufficient balance
404model_not_foundModel does not exist or is inactive
404generation_not_foundA UUID in generation_ids does not exist or doesn't belong to the caller
408upstream_timeoutRequest timed out (pre-deduction refunded)
409generation_not_readyReferenced generation_id is still being processed — retry later
413image_too_largeA single data URI string exceeds 35MB (rejected before billing)
422generation_not_readyReferenced generation_id belongs to a failed generation and cannot be used as a reference
429rate_limit_exceededRate limit exceeded
502upstream_errorService error (pre-deduction refunded)

Notes

  • b64_json is raw base64 (no data:image/png;base64, prefix) — decode directly with Buffer.from(b64, "base64") or Python base64.b64decode(b64)
  • Auto-persisted to history — generated images are automatically saved to Tokensmart history and queryable via /api/images/history. The picklyone_generation_ids in the response are the persisted UUIDs.
  • Chained composition — pass picklyone_generation_ids[i] from a prior call as a generation_id in the next call to iterate within the same context.

Related Endpoints