Spicy Schema contract
The shared request envelope, canonical fields, form schema, async lifecycle, errors, result storage and compatibility rules.
Spicy Schema is the public contract for every new media model. Adding a model changes its model, inputSchema and customer price; authentication, tasks, results, errors and downloads stay the same.
This is a reference page. Most integrations only need Model catalog and schemas. Read on if you are building a form, a validator or a code generator from inputSchema, and want to know exactly what you can rely on.
Only the live catalog proves callability
This page defines the contract shape; it does not claim that any model is live. Before paid work, read the authenticated GET /api/v1/models response and require both enabled: true and available: true. Planned or disabled entries are not callable.
Public boundary
A public model ID names one stable capability in the form <publisher>/<model>/<task>. The publisher is the model creator: the author or research organization behind the model. Image editing always takes the edit suffix, and identifiers published earlier stay accepted. Always copy a real ID from the live catalog; MODEL_ID_FROM_CATALOG on this page is a placeholder.
Public responses, webhooks, errors and downloads use only SpicyAPI's own identifiers, error codes and URLs. They never carry internal processing details, raw responses, raw errors or internal business data. Results are copied into SpicyAPI's own storage before they are returned.
Unified creation envelope
{
"model": "MODEL_ID_FROM_CATALOG",
"input": {
"prompt": "slow camera push-in",
"image_url": "spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3",
"duration_seconds": 8,
"resolution": "1080p",
"generate_audio": true
},
"callBackUrl": "https://your-app.example.com/hooks/spicy"
}| Field | Rule |
|---|---|
model | Exact, currently callable value from the authenticated live catalog |
input | Must satisfy that same item's current inputSchema; unknown fields are rejected |
callBackUrl | Optional public HTTPS address that receives the finished task; this exact casing. See Webhooks |
The input above is illustrative: which of these fields a model accepts is defined only by its own inputSchema.
Canonical fields
The same concept uses one snake_case name across all new endpoints. This table lists common fields, not all of them; a model's inputSchema is always the complete list.
| Concept | Field | Type |
|---|---|---|
| Positive prompt | prompt | string |
| Negative prompt | negative_prompt | string |
| Primary / final image | image_url / last_image_url | image reference (see below) |
| Several input images | image_urls | array of image references |
| Edit mask | mask_url | image reference |
| Primary video / audio | video_url / audio_url | public HTTPS URL or committed spicy:// URI |
| Reference media | reference_image_urls / reference_video_urls / reference_audio_urls | array of references |
| Output duration | duration_seconds | number or integer |
| Resolution / ratio | resolution / aspect_ratio | documented enum |
| Exact output size | size (a preset), or width / height | documented enum / integer |
| Frame rate | fps | integer |
| Native audio | generate_audio | boolean |
| Random seed | seed | integer |
| Adapter weights | loras, or high_noise_loras / low_noise_loras when an endpoint exposes its stages separately | array of objects |
| Output count | num_outputs | positive integer |
| Encoding | output_format | documented enum |
| Chat conversation | messages | array |
| Output length / sampling | max_tokens / temperature | integer / number |
An image reference is a public HTTPS URL, a committed spicy:// URI from an upload, or a small data:image/…;base64,… URI. Video and audio cannot be sent inline. See Media.
An adapter-weights entry names one set of weights and how strongly to apply it. The field name is fixed, the entry shape is not: what an endpoint accepts for a weight file — a URL, or a key from a built-in library — is narrowed by that endpoint's own items subschema, so read it there instead of assuming one form. The same goes for how many entries fit and which sub-fields are required.
A model-specific field exists only when it represents a real capability that no canonical field already covers, and it is named after that capability. An older endpoint may keep earlier field names in its live inputSchema; new endpoints do not copy them.
inputSchema and UI annotations
Every model uses JSON Schema 2020-12 in inputSchema. The same schema drives server validation, Playground forms, examples, and quotes. The official TypeScript SDK statically types the common request envelopes and responses; each model's input remains Record<string, unknown> and must be validated against the live schema rather than assumed to have generated per-model fields.
This site keeps no per-model parameter table. Parameters have exactly one source: the inputSchema returned by GET /api/v1/models?includeSchema=1, which is also what the Playground renders. A static copy drifts as models change, and nothing can catch that drift. To build your own table from it, see Model catalog and schemas.
Every schema you receive:
- has an
objectroot and rejects unknown fields (additionalProperties: false); - states
required, types, enums, ranges, lengths, array limits, URI formats, defaults and verified conditions; - gives every property a non-empty English
description; - leaves out fields and defaults that have not been verified, rather than guessing;
- expresses conditions through root-level
anyOf/allOf(below), never through UI visibility alone; - contains no switches that only change internal processing, and no content-filter switches.
Presentation annotations: x-ui
Presentation metadata is not a second validation schema. All of it lives in the property's own x-ui object, whose keys are snake_case and drawn from a closed set:
| Key | Meaning |
|---|---|
widget | control to render; values below |
order | the field order, descending first, unique within an endpoint |
label / placeholder | override the default label; input placeholder text |
rows | textarea row count |
step / unit | numeric step and unit suffix (s, px) |
accept / max_size_mb | MIME filter and per-file size cap for upload controls |
advanced | belongs in a collapsed advanced section |
primary | main input; pinned to the top and never collapsed |
affects_price | changing this value requires a fresh quote |
visible_when | conditional visibility expression |
enum_labels | enum value to display label |
x-ui.widget is one of thirteen values: textarea, text, upload, multi-upload, select, radio, slider, number, switch, json, object-list, chat-messages, hidden. A widget is always consistent with the data shape, so a renderer can dispatch on it directly: select and radio always carry an enum; slider always carries both minimum and maximum (never only exclusiveMinimum / exclusiveMaximum); switch is always a boolean; multi-upload is an array with items.type: string; object-list is an array with items.type: object and items.properties, each sub-field carrying its own type and description; chat-messages is an array; hidden means the field is still submitted but has no control.
For any array field, the array's own minItems / maxItems and its items subschema carry constraints that the root required list never mentions, and the server enforces them exactly like the rest of the schema. An object-list's items is a complete object schema in its own right: its own required, its own additionalProperties: false, and its own type, range and default for each sub-field. Defaults reach inside those entries: a sub-field you leave out is filled in from its default before validation runs, so a sub-field that is both required and has a default is effectively optional. One without a default is rejected when missing, as is an unknown sub-field or a value outside a sub-field's range.
A 400 from a value inside an array points at the exact entry — the field, the zero-based index and the sub-field — rather than at the request as a whole. Pass that pointer through to whoever has to fix it: with several entries in flight, knowing which one is out of range is the difference between a one-line fix and a bisect.
Clients must ignore unknown presentation keys: adding an x-ui key is a backward-compatible change.
{
"type": "object",
"required": ["prompt", "duration_seconds"],
"additionalProperties": false,
"properties": {
"prompt": {
"type": "string",
"minLength": 1,
"description": "Describes the scene and its motion.",
"x-ui": { "widget": "textarea", "order": 100, "primary": true, "rows": 4 }
},
"duration_seconds": {
"type": "integer",
"enum": [5, 8, 10],
"description": "Output duration in seconds.",
"x-ui": { "widget": "radio", "order": 90, "unit": "s", "affects_price": true }
}
},
"x-pricing": { "variantFields": ["duration_seconds"], "billingMode": "variant_matrix" }
}Every description is English prose and can be displayed verbatim as the field's help text.
x-ui.affects_price: true tells the UI to refresh a quote after the field changes. It is not a pricing formula and never replaces catalog pricing or quantityField. Root x-pricing is the server-side view of the same fact: variantFields lists the fields that select a price tier and is always a subset of the affects_price: true fields, while billingMode says how the endpoint is metered. Both are hints; the authoritative amount is the one returned by the catalog, the quote and the task. How tiers appear in pricing is described in Reading pricing.
Conditional fields
Conditions take exactly two shapes, both at the root:
{
"anyOf": [{ "required": ["image_url"] }, { "required": ["image_urls"] }],
"allOf": [
{ "if": { "required": ["last_image_url"] }, "then": { "required": ["image_url"] } },
{ "if": { "properties": { "resolution": { "const": "4k" } } }, "then": { "required": ["seed"] } }
]
}Each anyOf branch contains only required and reads as "supply at least one of these". Each allOf branch is one if → then: if triggers on required or properties.<field>.const, then declares only required, and it reads as "if you supply A you must supply B".
oneOf, not, dependentSchemas and dependentRequired never appear in a model's inputSchema: they express the constraint but cannot be rendered as a form, so the user would only learn about it as an unexplainable 400 after submitting. Implementing the two shapes above covers every conditional endpoint.
Async lifecycle
queued -> running -> succeeded
-> failed
-> expiredcreateTask returns a Spicy task ID. Poll recordInfo or receive a signed Spicy webhook. queued and running are non-terminal; the others are terminal. Historical canceled records remain readable for compatibility, but new tasks have no cancellation action. Asynchronous tasks is the authority on states.
An unknown non-terminal state inside a model is mapped to running until a known terminal state or the absolute platform deadline. A non-idempotent submission with an uncertain acknowledgment is not blindly replayed, preventing duplicate generation and charges.
Unified response and errors
Every native JSON API (/api/v1) uses the same envelope; the /v1 and /v1beta compatibility endpoints use their protocols' own shapes:
{
"code": 200,
"msg": "success",
"data": {},
"request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}data is absent on synchronous failure. Branch on code, never on the changeable msg; use request_id for support. A failure after task creation uses data.state: "failed" and a stable errorCode. errorMessage is human-readable: when the model service reported a specific reason, it is that reason in English with internal names, addresses and account details removed; otherwise it is a platform message in your error language. Branch on errorCode, never on errorMessage.
Results and downloads
A successful task lists its files in output.assets[]. Each ready asset already carries a download link:
{
"assets": [
{
"key": "tasks/2026/08/31/job_01k3m8x9q2z4v7n5p6r8s0t1w3/asset_01.mp4",
"url": "https://example.r2.cloudflarestorage.com/results/asset_01.mp4?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
"expiresAt": "2026-08-31T10:20:00Z",
"mime": "video/mp4",
"durationSeconds": 8,
"bytes": 1234567
}
]
}Download url directly, without your API key. It normally lasts 20 minutes; read the task again for a fresh one while the result is retained (at most 14 days). While a file is still being stored, the asset has pending: true and no url; if storing it failed for good, it has unavailable: true. key identifies the file for the optional download-url endpoint; you do not need it to download. All links point to SpicyAPI's own storage.
Customer billing
Catalog pricing, startingPrice, quantityField and task estimatedCost / cost describe customer pricing only. Amounts are decimal USD strings. Internal business data is outside the public contract and never appears in catalogs, tasks, webhooks or exported logs.
Compatibility and deprecation
Changes you should tolerate without breaking: a new optional field, a new response field you can ignore, or an unknown x-ui hint.
A new required field, a removed enum value, a change of meaning or type, a tighter limit, or a changed public model ID comes with a new model version or an announced deprecation window. Published IDs stay immutable; migrations use a compatibility alias or an explicit redirect.
Cache the authenticated catalog as described in Caching the catalog. Never keep a permanent schema snapshot or fall back to a static page when the live catalog has no callable item.

