Image generation
POST https://api-staging.heimdallm.com/api/v1/images
A prompt in, images out, on the reference Image API. Requires a bearer API key.
Use a model whose output modality is image — filter the model list or look for
"output_modalities": ["image"] in the catalogue.
Reference images are supported only by endpoints that declare image conditioning.
Generations are slower than completions — tens of seconds is normal — so set a client timeout that
expects it. stream: true relays the render as it is drawn on an endpoint that declares it; every
other endpoint answers with the completed image alone.
The shape you asked for
Geometry is named three ways: a resolution tier (resolution), an aspect ratio (aspect_ratio), or
explicit pixels (size). Whichever you send is resolved against each endpoint's image_envelope into
the one WIDTHxHEIGHT that endpoint is asked for, held against and billed on, snapped to its own
edge_multiple: 1K at 16:9 on a 16-pixel grid is 1024x576.
Pixels named beside a tier or a ratio are two answers to one question, and are refused with 400.
Each endpoint publishes the bounds it draws inside as image_envelope in the
catalogue, and every shape inside them is served:
| Field | Means |
|---|---|
max_megapixels | The most one image may come to. Width × height ÷ 1,000,000 must not exceed it. |
min_edge | The shortest a single edge may be, in pixels. |
max_edge | The longest a single edge may be, in pixels. |
edge_multiple | Both edges must be a multiple of this. |
The area limit describes the shapes the engine accepts. It does not select a billing unit: an endpoint can charge per output image or per megapixel, with an explicit rate for the requested settings.
The edges are a second and different limit. A 8192×488 sliver is under 4 megapixels and would fit any budget a squarer render of the same area fits, and no diffusion model produces anything worth paying for at that ratio — the positional encoding degrades long before the memory does. Clearing the pixel budget does not imply clearing the edges, or the other way round.
The grid is the model's own architecture rather than a policy: a diffusion model whose VAE downsamples by 8 under a transformer that patches by 2 can only place its latents on a 16-pixel grid.
A request outside the envelope is routed nowhere rather than resized to fit, and the error names the
sizes on either side of what you asked for. Asking a 16-pixel-grid endpoint for 1000x1000 comes
back naming 992x992 and 1008x1008. Nothing is resized to fit: a different shape would not fulfil
the request. A tier no endpoint of the model draws is refused naming the bound it crossed.
The endpoint’s declared tariff determines the charge. A per-image price counts delivered pictures; a linear megapixel price counts their area. Size-dependent prices carry explicit conditions. See what you are charged.
Reference images
On models that declare image conditioning, send input_references with one to three
{type: "image_url", image_url: {url: "data:image/png;base64,BASE64_BYTES"}} parts. PNG, JPEG and WebP
references must total at most 20 MiB. External URLs are not fetched: an https: reference parses
and is refused where you can still act on it, because the marketplace fetches nothing on your behalf.
The endpoint's reference_inputs.image declaration lists accepted formats and its reference count limit.
An unsupported reference is refused before inference; models declaring only text input cannot edit images.
Each item may carry a role saying what it is for: reference for a picture to edit or keep the
look of, and the control passes canny, depth, normal, openpose and segmentation that the
render keeps the structure of. A pass is named for the preprocessor that drew it, in the spelling
ControlNet, ComfyUI and diffusers use, and the shorter edges, pose and seg are accepted as the
same three passes. An item without a role is a reference; a model that does not accept
reference refuses the request and names the roles it accepts. reference_inputs.image.roles lists
the roles the endpoint takes and how many items of each one request may carry, and
reference_inputs.image.control_sets, where it is published, lists which control passes may travel
together.
To animate an image, choose Animate on its result in /image, or submit it as a video reference to
POST /videos.
Request body
model and prompt are required, and there is no models fallback list.
| Field | Type | Notes |
|---|---|---|
model | string | Required. |
prompt | string | Required. What to draw. |
n | integer | 1 to 10. An upper bound: an endpoint may return fewer. Each delivered image is billed. |
resolution | string | Tiers 512, 1K, 2K, 4K. |
aspect_ratio | string | Ratios 1:1, 16:9, 9:21, auto, … |
size | string | WIDTHxHEIGHT, auto, or a tier. |
quality | string | auto, low, medium, high, xhigh or max. Ignored by an endpoint with no such knob. |
output_format | string | png, jpeg, webp or svg. svg is UTF-8 base64-encoded markup in b64_json. |
background | string | auto, transparent or opaque. transparent needs an output_format carrying alpha. |
output_compression | integer | 0 to 100, for webp/jpeg. Ignored for png. |
seed | integer | Deterministic where the endpoint supports it. |
stream | boolean | Relays the render as it is drawn, where the endpoint declares it. |
input_references | array | Up to 16 inline image references, up to 20 MiB total, where the endpoint declares support. |
role (per item) | string | What the item is for. Omitted, it is reference. Only roles the endpoint declares are accepted. |
provider | object | Routing preferences plus options keyed by provider slug for provider-specific passthrough. |
user | string | Folded into a hashed, per-account identifier before it reaches a provider. |
Endpoints differ in which of these they honour: each publishes its own supported_parameters in the
catalogue and on the endpoints listing, and that list is the promise.
A value outside the domain an endpoint publishes for a member is refused before dispatch rather than
by an upstream 400.
A provider-specific knob — a diffusion sampler's step count, for instance — is never a top-level
member on this contract. It travels through provider.options, keyed by the provider's own slug, so
that a member every other provider would have to ignore never appears on the request at all:
{
"model": "bytedance-seed/seedream-4.5",
"prompt": "a red panda astronaut floating in space, studio lighting",
"provider": { "options": { "black-forest-labs": { "guidance": 3, "steps": 40 } } }
}
Images come back inline
Hosted image URLs would mean the marketplace storing your generations, and it stores nothing — so the
bytes travel in the reply as base64 under b64_json and they are yours to keep or discard. There is
no member for asking otherwise.
Response
{
"created": 1748372400,
"data": [{ "b64_json": "<base64-encoded-image>", "media_type": "image/png" }],
"usage": { "prompt_tokens": 0, "completion_tokens": 4175, "total_tokens": 4175, "cost": 0.04 }
}
media_type on a data item is the container the bytes actually arrived in, read from them rather
than restated from what was asked for; an svg render carries image/svg+xml. There is no per-image
size or seed: shape is read from the bytes. usage follows the chat usage shape, with cost in
USD, and a count the endpoint never reported is absent rather than zero.
There is no id, model or provider member on the body — a generation id and the serving provider's
name travel on X-Generation-Id and X-Provider-Name, exactly as they do for every dialect that
leaves them off the body.
Streaming events
stream: true relays the render as the engine draws it, on an endpoint whose supports_streaming is
true; one that declares nothing answers with the completed image alone, which is a stream of one
event rather than a broken promise. A stream carries one JSON object per data: line — nothing wraps
it — and ends on a literal data: [DONE] line rather than the connection closing:
data: {"type":"image_generation.partial_image","partial_image_index":0,"b64_json":"<base64>"}
data: {"type":"image_generation.completed","b64_json":"<base64>","media_type":"image/png","created":1748372400,"usage":{"prompt_tokens":0,"completion_tokens":4175,"total_tokens":4175,"cost":0.04}}
data: [DONE]
A text-based format streams markup fragments as image_generation.text_chunk events, whose phase
is content for renderable output and reasoning or draft for an intermediate provider phase.
An error event carries the same shape a failed request answers with, and ends the stream without a
[DONE] line:
data: {"type":"error","error":{"message":"Generation failed","code":"server_error"}}
What you are charged
Billing is all or nothing. The charge is computed once, from the images that completed, and rides out
on the reply or on the last image_generation.completed event and nowhere else. A render that failed,
or whose caller left, settles nothing at all: the previews already delivered are yours, and nobody is
charged for them. A client that stopped reading after the first picture of several has been told about
no charge, which is the truthful reading while the rest were still to come.
Read the endpoint’s pricing unit and conditions before generating:
- Per output image: the
image_outputrate is multiplied by the number of delivered images. Two images cost twice the single-image rate, unless a declared condition selects a different rate. - Per megapixel:
image_megapixelis multiplied by the delivered area divided by 1,000,000. A 1024×1024 image consumes 1.048576 megapixels; the quantity is not rounded to whole megapixels. - Per image token: this route cannot establish a finite image-token reservation bound, so a positive input- or output-image-token tariff is refused before dispatch. An image count, pixel area, or text context limit cannot substitute for that bound. Publishing a token tariff in the catalogue does not make it dispatchable here.
usage.cost is the actual USD charge under the selected tariff. Area is a geometry measurement and
does not mean the endpoint bills by area.
Declared conditions can change the rate for supported settings such as size or quality. The selected rate is frozen for the request and used for reservation, settlement and recovery. A conditional resolution label is not inferred from arbitrary pixel dimensions.
Text and image input tokens are separate quantities where the provider reports them. Cache counts must belong to the appropriate input partition; combined totals are not charged again as additional input. Missing usage is unknown, never a measured zero. An engine that reports no token usage should omit token tariffs rather than publish an invented zero rate.
Example
curl ${API_BASE_URL}/images \
-H "Authorization: Bearer $HEIMDALL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen/qwen-image",
"prompt": "A brass ship'"'"'s compass on a folded nautical chart, morning light",
"size": "1024x1024",
"n": 1,
"quality": "high"
}'
Errors
The error model is the same as everywhere. Two are particular to this endpoint:
- 503 — the model exists, and no eligible endpoint draws the shape you asked for. The error names
which bound turned it away and the nearest sizes that clear it;
image_envelopein the catalogue has the bounds themselves. - 504 — the endpoint did not finish in time. Generations are long, and a busy endpoint queues; retry
or send
providerpreferences that route elsewhere.
Listings
GET https://api-staging.heimdallm.com/api/v1/images/models
Every served model with an image output, the model list narrowed and restated in
this contract's terms: id, name, description, created, architecture (input_modalities,
output_modalities), supported_parameters, supports_streaming and a relative endpoints URL.
offset and limit page it.
supported_parameters is a map of typed descriptors rather than a bare name list — an enum
descriptor names the values accepted, a range descriptor its inclusive bounds, and a boolean
descriptor that the member is accepted at all:
{
"n": { "type": "range", "min": 1, "max": 10 },
"aspect_ratio": { "type": "enum", "values": ["1:1", "16:9", "auto"] },
"seed": { "type": "boolean" }
}
Both supported_parameters and supports_streaming are the union across the model's live endpoints,
which makes them a discovery aid rather than a promise any one endpoint keeps. The endpoints listing
is what answers per endpoint:
GET https://api-staging.heimdallm.com/api/v1/images/models/{author}/{slug}/endpoints
Each entry carries the provider's name and slug, the provider_tag a provider.only, provider.ignore
or provider.order entry names, that endpoint's own supported_parameters, its
allowed_passthrough_parameters (the keys it accepts under provider.options[provider_slug]), whether
it supports_streaming, its discount, and a pricing array. One pricing line names one billable unit:
| Field | Notes |
|---|---|
billable | output_image, input_image, input_reference, input_text or input_font. |
unit | request, image, megapixel or token. |
cost_usd | USD, as a number — the one money figure on this contract the reference itself spells that way. |
variant | Distinguishes lines sharing a billable and unit, such as a resolution-tiered price. |
overrides is a Heimdallm extension containing ordered { when, cost_usd } declarations. The last
matching condition selects the rate. sku_type and pricing_scope retain the declared charge
identity, including separate prompt and cache prices. Every line is the rate a request is charged,
and the endpoint’s discount is the fraction taken off its list to reach them, so a list rate is
cost_usd / (1 - discount); it is always present, 0 where nothing is taken off and negative where
the endpoint charges above its list. The fraction sits beside the prices: inside pricing where it
is an object, as on the model and endpoint listings, and on the endpoint here, where pricing is an
array of lines with no room beside them for a member that is not a line. utc_start and utc_end
are UTC HHMM clock values;
utc_days, ttl_seconds and implicit retain applicable qualifiers. A base rate accompanied by
conditions is a declaration, not an unconditional quote. The general catalogue omits a scalar media
price where its conditions cannot be represented truthfully.
A line is published only for a unit the endpoint priced. A short pricing array is a partial
declaration, never a discount: a unit with no line is a unit nobody published a price for, and reading
it as free would be reading an absence as a zero. A declared 0 is a real price and appears as 0.
A model served by nothing answers 200 with an empty endpoints array. A model with no image output
is not on this route and answers 404.
{
"id": "acme/paint-v1",
"endpoints": [
{
"discount": 0,
"provider_name": "Epsilon Render",
"provider_slug": "epsilon",
"provider_id": "7d2e9a40-3c5b-4e1f-8a6d-2b9c0f1e4d3a",
"provider_tag": "epsilon",
"supported_parameters": { "seed": { "type": "boolean" }, "size": { "type": "boolean" } },
"allowed_passthrough_parameters": [],
"supports_streaming": false,
"pricing": [{ "billable": "output_image", "unit": "megapixel", "cost_usd": 0.002 }]
}
]
}
Provider adapters
Endpoint adapters.image catalogues expose provider-hosted LoRAs and their immutable revisions,
strength bounds, compatibility and pricing mode. Use provider.options[provider_slug].adapters
with { id, revision, strength } selections and pin provider.only to that provider. Up to four
adapters may be selected within the declared combination constraints. Providers cache the weights;
there is no customer weight upload. A stale revision or unsupported selection is refused before
dispatch. Conditional adapters change the generation tariff; consult endpoint pricing_skus for
rates and conditions before submitting.
The image CLI accepts a JSON array of selections:
heimdall image "A quiet waterfront at dusk" --model AUTHOR/MODEL --provider PROVIDER \
--adapters selections.json --out waterfront.png