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.

Models Version
Get $5 Free Credit on First Payment
No strings attached — add funds and get $5 bonus instantly
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.
| Header | Type | Required | Description |
|---|---|---|---|
| Ocp-Apim-Subscription-Key | string | Yes | Your 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_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
- Send a generate request to the API endpoint
- Save the
request_idfrom the response - Poll every 5-10 seconds:
GET /v2/requests/status/{request_id} - When
statusis"COMPLETED", download fromoutput.media_url
Tip: Use X-Webhook-URL header to get a callback instead of polling.
Compose Images 1.0 API Pricing
⚡ 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).
〰 Uptime
Percent of generations that succeeded over the selected period, per model version.