Pixazo APIImage Tools APICompose Images
Pixazo APIImage Tools APICompose Images

Compose Images 1 API: Pricing, Documentation

by Pixazo

Join 2 to 12 images into one PNG — side by side with layout horizontal, stacked with vertical, or tiled with grid — and set the spacing between them with gap_px (0 to 200). The size budget is checked from each file’s header before anything is decoded: 8 MP per image and 12 MP for the finished sheet. A file whose header cannot be read is refused rather than attempted, so an oversized set fails fast instead of timing out.

Get API Key
Compose Images API

Models Version

WELCOME BONUS

Get $5 Free Credit on First Payment

No strings attached — add funds and get $5 bonus instantly

Claim Your $5 →

Compose Images 1.0 API Documentation

https://gateway.pixazo.ai/media-tools/v1/compose-images

Authentication

All requests require an API key passed via header.

Pricing: Billed at $0.002 per call — a flat rate. The number of images does not change the price: composing 2 images and composing 12 both cost $0.002.

Retries: this is an asynchronous job on shared encoding capacity, so a request can occasionally come back processing_failed or take much longer than usual. These are transient and succeed on a retry, and a failed job is never charged — the wallet hold is released. If you chain these tools, retry a failed step rather than failing the whole pipeline.

HeaderTypeRequiredDescription
Ocp-Apim-Subscription-KeystringYesYour API subscription key

Compose Images generate request

Request Code

POST https://gateway.pixazo.ai/media-tools/v1/compose-images
Content-Type: application/json
Cache-Control: no-cache
Ocp-Apim-Subscription-Key: YOUR_API_KEY

{
  "image_urls": [
    "https://api-assets.pixazo.ai/media-api-test/a.png",
    "https://api-assets.pixazo.ai/media-api-test/b.png"
  ],
  "layout": "horizontal",
  "gap_px": 12
}
import requests

url = "https://gateway.pixazo.ai/media-tools/v1/compose-images"
headers = {
    "Content-Type": "application/json",
    "Cache-Control": "no-cache",
    "Ocp-Apim-Subscription-Key": "YOUR_API_KEY"
}
data = {
  "image_urls": [
    "https://api-assets.pixazo.ai/media-api-test/a.png",
    "https://api-assets.pixazo.ai/media-api-test/b.png"
  ],
  "layout": "horizontal",
  "gap_px": 12
}

response = requests.post(url, json=data, headers=headers)
print(response.json())
const url = "https://gateway.pixazo.ai/media-tools/v1/compose-images";
const headers = {
  "Content-Type": "application/json",
  "Cache-Control": "no-cache",
  "Ocp-Apim-Subscription-Key": "YOUR_API_KEY"
};
const data = {
  "image_urls": [
    "https://api-assets.pixazo.ai/media-api-test/a.png",
    "https://api-assets.pixazo.ai/media-api-test/b.png"
  ],
  "layout": "horizontal",
  "gap_px": 12
};

fetch(url, {
  method: "POST",
  headers: headers,
  body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => console.log(data));
curl -X POST "https://gateway.pixazo.ai/media-tools/v1/compose-images" \
  -H "Content-Type: application/json" \
  -H "Cache-Control: no-cache" \
  -H "Ocp-Apim-Subscription-Key: YOUR_API_KEY" \
  --data-raw '{
    "image_urls": [
      "https://api-assets.pixazo.ai/media-api-test/a.png",
      "https://api-assets.pixazo.ai/media-api-test/b.png"
    ],
    "layout": "horizontal",
    "gap_px": 12
  }'

Output

{
  "request_id": "media-compose-images_019dxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "status": "QUEUED",
  "polling_url": "https://gateway.pixazo.ai/v2/requests/status/media-compose-images_019dxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Webhook (Optional)

Add the X-Webhook-URL header to your generate request to receive a POST callback instead of polling.

X-Webhook-URL: https://your-server.com/webhook/callback

Request Parameters - Compose Images generate request

Parameter Required Type Default Allowed values / range Description
image_urlsYesarray of strings2 to 12 public http(s) URLsThe images to composite, in the order they will appear. Every element must be a string beginning http:// or https://; a nested array, a bare filename or a file: URL is rejected. Fewer than 2 or more than 12 entries is refused before any wallet hold — see the note below.
layoutNostringhorizontalhorizontal, vertical, gridHow the images are arranged. horizontal places them left to right, vertical top to bottom, grid fills a near-square grid row by row. Any other value is refused before any wallet hold.
gap_pxNonumber00 – 200Width in pixels of the white spacing added around each image on the stacking axis. Honoured by horizontal and vertical only — grid ignores it. A value outside 0–200 is refused before any wallet hold.

What this endpoint actually does: it downloads your images and composites them into one PNG with ImageMagick. It is deterministic, not generative — no model is called, nothing is invented or restyled, and the same inputs always produce the same output. image_urls, layout and gap_px are the whole API: there is no prompt, no width/height, no background colour and no per-image placement. An unknown parameter is rejected rather than ignored.

Three of your mistakes are free. The array count, the layout value and the gap_px range are all settled from your request body alone, at the gateway, before the wallet hold is placed. Sending 1 URL, 13 URLs, layout: "diagonal" or gap_px: 500 comes straight back as a synchronous 400 and costs nothing, so probing those limits is free.

Pixel budget: 8 MP per image, 12 MP combined. Unlike the three checks above, this one cannot be settled from your parameters — the size of an image is not something a URL states — so it is measured after the fetch, by reading each file’s header only (identify -ping, no decode) and before any compositing work. Any single input over 8 megapixels, or a set totalling more than 12 megapixels, is refused. Because it runs after the submit has been accepted, the verdict arrives on the polled status as a FAILED job, not as a 400 — see Compose Verdicts below. Practically: twelve 1 MP thumbnails fit comfortably; two 8 MP photos do not, because together they are 16 MP.

The header read fails closed. If a file’s header cannot be read — it is not an image, it is truncated, or the URL served an HTML error page — the request is rejected, not guessed at, and the error names the offending index (image_urls[2]). Nothing is composited on a maybe.

Layout semantics. horizontal and vertical keep every image at its native pixel size and join them in array order; where the images disagree on the cross axis — differing heights in a horizontal strip — the shorter ones are padded with white rather than scaled, so a mismatched set gives you a ragged strip. grid is the opposite: it computes a near-square arrangement (4 images give 2×2, 5 give 3×2, 12 give 4×3), takes its cell size from the first image in the array, and resizes every other image to cover that cell, centred and cropped — so in grid mode the first URL you send decides the shape of every tile, and a short final row is padded with white.

How gap_px is applied. It is a white border added to each image on the stacking axis, not a single divider drawn between them. The visible space between two neighbours is therefore twice gap_px, and the outer edges of the finished image carry gap_px of white as well. Send gap_px: 12 for a 24 px seam.

Output: always a single PNG, whatever the input formats. Transparency in an input is composited over white where the layout has to pad or crop. The output is returned as one file at output.media_url[0] with output.media_type of image/png.

Example Request

{
  "image_urls": [
    "https://api-assets.pixazo.ai/media-api-test/a.png",
    "https://api-assets.pixazo.ai/media-api-test/b.png"
  ],
  "layout": "horizontal",
  "gap_px": 12
}

Response

{
  "request_id": "media-compose-images_019dxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "status": "QUEUED",
  "polling_url": "https://gateway.pixazo.ai/v2/requests/status/media-compose-images_019dxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Request Headers

Header Value
Content-Typeapplication/json
Cache-Controlno-cache
Ocp-Apim-Subscription-KeyYOUR_API_KEY

Response Handling

Common status codes.

CodeMeaning
202Accepted — Request queued
Bad Request — includes a bad image_urls count, an unknown layout and an out-of-range gap_px, all settled before any charge
401Unauthorized
402Insufficient Balance
403Forbidden
Too Many Requests
500Internal Server Error

Error Responses

Queue system errors and model validation errors.

Queue System Errors

// 402 — Insufficient balance
{
  "error": "Insufficient Balance",
  "message": "Your wallet does not have enough balance."
}
// 400 — Model not found
{
  "error": "Model not found",
  "message": "Model 'media-compose-images' not found or is disabled"
}
// 400 — the array count, layout or gap_px is outside what this
// endpoint accepts. Settled at the gateway from your parameters alone,
// BEFORE any wallet hold, so probing these limits costs nothing.
{
  "error": "Invalid request parameters",
  "message": "image_urls must be an array of 2 to 12 public http(s) URLs.",
  "violations": ["image_urls must be an array of 2 to 12 public http(s) URLs."]
}

Error via Status/Webhook

{
  "request_id": "media-compose-images_019dxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "status": "ERROR",
  "model_id": "media-compose-images",
  "error": "Description of the error",
  "output": null
}

Compose Verdicts

The pixel budget is decided after the images have been fetched and their headers read, so it is not a submit-time error: the submit returns 202 as usual and the verdict arrives on the polled status response as a FAILED job. The error string carries the code invalid_input followed by the sentence for that verdict, with the numbers for your images in it. A verdict is the caller’s to act on, so the job is not charged — the hold is released.

// GET /v2/requests/status/{request_id}
// One input is over the 8 MP per-image ceiling
{
  "request_id": "media-compose-images_019dxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "status": "FAILED",
  "model_id": "media-compose-images",
  "error": "invalid_input: image_urls[0] is 4000x3000 (12.0 MP); each image must be under 8 MP.",
  "output": null
}
VerdictWhen it firesThe sentence you get
Image not readable An entry downloaded, but its header could not be parsed as an image — a truncated file, a PDF, or an HTML error page served at that URL. Checked first, per entry, so the index in the message is the one to look at. image_urls[2] is not a readable image.
Single image over 8 MP Any one input whose width × height exceeds 8 megapixels. Tested per entry as the set is read, so the first oversized image is the one reported. image_urls[0] is 4000x3000 (12.0 MP); each image must be under 8 MP.
Set over 12 MP combined Every image is individually under 8 MP but their pixel counts add up to more than 12 megapixels. This is the ceiling a set of large-but-legal photos meets: two 8 MP images pass the per-image test and fail here. inputs total 14.3 MP; the combined limit is 12 MP.

The numbers in each sentence are computed for your images, so treat the examples above as shapes rather than fixed strings — match on the invalid_input code, not on the wording. To stay inside the budget, downscale your inputs before sending them: the composite is built at the inputs’ own resolution, so oversized sources buy you nothing.

Retrieving Results

Poll the universal status endpoint to check progress and retrieve results.

Endpoint

GET https://gateway.pixazo.ai/v2/requests/status/{request_id}
Ocp-Apim-Subscription-Key: YOUR_API_KEY

cURL Example

curl -H "Ocp-Apim-Subscription-Key: YOUR_API_KEY" \
  "https://gateway.pixazo.ai/v2/requests/status/media-compose-images_019dxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

Response (Completed)

{
  "request_id": "media-compose-images_019dxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "status": "COMPLETED",
  "model_id": "media-compose-images",
  "error": null,
  "output": {
    "media_url": [
      "https://pub-582b7213209642b9b995c96c95a30381.r2.dev/v1/media-compose-images_019dxxxx/output.png"
    ],
    "media_type": "image/png"
  },
  "created_at": "2026-03-31T10:00:00.000Z",
  "updated_at": "2026-03-31T10:00:09.000Z",
  "completed_at": "2026-03-31T10:00:09.000Z"
}

Response Fields

FieldTypeDescription
request_idstringUnique request identifier
statusstringQUEUED, PROCESSING, COMPLETED, FAILED, or ERROR
model_idstringModel that processed the request
errorstring|nullError message if failed
output.media_urlarrayURL of the composited PNG (R2 CDN) — always a single entry
output.media_typestringMIME type of the output — always image/png
created_atstringWhen request was created
completed_atstringWhen request completed
polling_urlstringStatus URL (initial response only)

Status Values

StatusDescription
QUEUEDRequest accepted, waiting to be processed
PROCESSINGBeing processed by the model
COMPLETEDDone — output contains the result
FAILEDFailed — check error field
ERRORSystem error — not charged

Status Flow

QUEUED → PROCESSING → COMPLETED
                    → FAILED
                    → ERROR

Typical Workflow

  1. Send a generate request to the API endpoint
  2. Save the request_id from the response
  3. Poll every 5-10 seconds: GET /v2/requests/status/{request_id}
  4. When status is "COMPLETED", download from output.media_url

Tip: Use X-Webhook-URL header to get a callback instead of polling.

Compose Images 1.0 API Pricing

Your request will cost $0.002 per call.
every compose costs $0.002, whether you send 2 images or 12

⚡ Performance

Live usage measured on Pixazo's gateway, split by model version. Generation time is how long a generation takes end-to-end (lower is better). Success rate is the percent of generations that complete (higher is better).

Show data for the last
Generations
200last 30d
~7 per day
Success rate
50.0%
of completed generations
Generation time
19.9savg
p95 19.9s
Requests
Aug 22max 200Sep 20
Compose Images 1.0Avg 7/day
Generation Time
Aug 22max 19.9sSep 20
Compose Images 1.0Avg 19.9s
Error Rate
Aug 22max 50.0%Sep 20
Compose Images 1.0Avg 50.0%

〰 Uptime

Percent of generations that succeeded over the selected period, per model version.

Avg. Success Rate (30d)
50.00%
across all generations of this model family
Uptime
Aug 22max 100%Sep 20
SuccessfulAvg 50.00%