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 should change its model, inputSchema and customer price; authentication, tasks, results, errors and downloads stay stable.
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 <family>/<version-or-variant>/<task>. provider means the model creator: the author or research organization behind the model.
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 object storage before an object key is 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 HTTP(S) callback; v1 fixes this exact casing |
Canonical fields
The same concept uses one snake_case name across all new endpoints:
| Concept | Field | Type |
|---|---|---|
| Positive prompt | prompt | string |
| Negative prompt | negative_prompt | string |
| Primary / final image | image_url / last_image_url | URI or committed spicy:// URI |
| Primary video / audio | video_url / audio_url | URI or committed spicy:// URI |
| Reference media | reference_image_urls / reference_video_urls / reference_audio_urls | URI array |
| Output duration | duration_seconds | number or integer |
| Resolution / ratio | resolution / aspect_ratio | documented enum |
| Native audio | generate_audio | boolean |
| Random seed | seed | integer |
| Output count | num_outputs | positive integer |
| Encoding | output_format | documented enum |
A model-specific advanced field is allowed only when it represents a real capability, does not duplicate a canonical concept and is named after the capability itself. A legacy endpoint may retain old fields in its live inputSchema; new endpoints must not copy those spellings.
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.
Each schema must:
- use an
objectroot and default toadditionalProperties: false; - state
required, types, enums, ranges, lengths, array limits, URI formats, defaults and verified conditions; - give every property a non-empty English
description; - omit unverified fields instead of guessing capabilities or defaults;
- express conditions through root-level
anyOf/allOf(below), not UI visibility alone; - omit switches that only change internal processing, and omit 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.
Spellings that never appear in any model's inputSchema: root x-order-properties (order comes only from x-ui.order), x-ui-component, x-ui.hint, and the camelCase x-ui.affectsPrice. Those were early conventions; package validation now rejects them and the server no longer emits them. Clients should still ignore unknown presentation extensions — adding an ignorable 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 and the task.
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.
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 JSON API uses the same envelope:
{
"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 a safe normalized explanation and contains no internal service name, host, task ID or raw error.
Results and downloads
Successful results contain first-party object keys and metadata only:
{
"assets": [
{
"key": "tasks/2026/08/31/job_.../asset_....mp4",
"mime": "video/mp4",
"durationSeconds": 8,
"bytes": 1234567
}
]
}output.assets[].key is not a URL. Exchange it through /api/v1/common/download-url for a short-lived signed address. While transfer is pending, an asset may have pending: true and an empty key; exhausted transfer retries produce unavailable: true. Public results are always served from 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
Backward-compatible changes include a new optional field, a response field clients can ignore, or an unknown x-ui hint.
A new required field, removed enum value, semantic or type change, tighter limit, or public model-ID change requires 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 for at most 60 seconds and invalidate by version / updatedAt. Never keep a permanent schema snapshot or fall back to a static page when the live catalog has no callable item.

