---
type: AI Model
id: image-compose-api
title: Compose Images API
provider: Pixazo
description: "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."
resource: https://www.pixazo.ai/models/image-compose-api
docs_url: https://www.pixazo.ai/models/image-compose-api
latest_version: v1
tags:
  - compose-images
  - pixazo
variants:
  - id: media-compose-images-v1
    name: Compose Images 1.0
    version: 1.0
    capabilities:
      - Compose
timestamp: 2026-09-19T12:37:58.999Z
---

# Compose Images API

> Provider: **Pixazo**
> Source: https://www.pixazo.ai/models/image-compose-api

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.

## Compose Images 1.0

### Compose

## Base URL

```
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.

Header

Type

Required

Description

Ocp-Apim-Subscription-Key

string

Yes

Your API subscription key

## Compose Images generate request

## Request Code

HTTP Python JavaScript cURL

```
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"
}
```

[Try Now](https://api.pixazo.ai/api-details#api=media-tools&operation=compose-images)

## 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\_urls

Yes

array of strings

—

**2 to 12** public `http(s)` URLs

The 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.

layout

No

string

`horizontal`

`horizontal`, `vertical`, `grid`

How 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\_px

No

number

`0`

0 – 200

Width 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-Type

application/json

Cache-Control

no-cache

Ocp-Apim-Subscription-Key

YOUR\_API\_KEY

## Response Handling

Common status codes.

Code

Meaning

202

Accepted — Request queued

400

Bad Request — includes a bad `image_urls` count, an unknown `layout` and an out-of-range `gap_px`, all settled before any charge

401

Unauthorized

402

Insufficient Balance

403

Forbidden

429

Too Many Requests

500

Internal 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
}
```

Verdict

When it fires

The 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

Field

Type

Description

request\_id

string

Unique request identifier

status

string

QUEUED, PROCESSING, COMPLETED, FAILED, or ERROR

model\_id

string

Model that processed the request

error

string|null

Error message if failed

output.media\_url

array

URL of the composited PNG (R2 CDN) — always a single entry

output.media\_type

string

MIME type of the output — always `image/png`

created\_at

string

When request was created

completed\_at

string

When request completed

polling\_url

string

Status URL (initial response only)

## Status Values

Status

Description

QUEUED

Request accepted, waiting to be processed

PROCESSING

Being processed by the model

COMPLETED

Done — output contains the result

FAILED

Failed — check error field

ERROR

System 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.
