spicyapiDocs
Main content

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"
}
FieldRule
modelExact, currently callable value from the authenticated live catalog
inputMust satisfy that same item's current inputSchema; unknown fields are rejected
callBackUrlOptional 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.

ConceptFieldType
Positive promptpromptstring
Negative promptnegative_promptstring
Primary / final imageimage_url / last_image_urlimage reference (see below)
Several input imagesimage_urlsarray of image references
Edit maskmask_urlimage reference
Primary video / audiovideo_url / audio_urlpublic HTTPS URL or committed spicy:// URI
Reference mediareference_image_urls / reference_video_urls / reference_audio_urlsarray of references
Output durationduration_secondsnumber or integer
Resolution / ratioresolution / aspect_ratiodocumented enum
Exact output sizesize (a preset), or width / heightdocumented enum / integer
Frame ratefpsinteger
Native audiogenerate_audioboolean
Random seedseedinteger
Adapter weightsloras, or high_noise_loras / low_noise_loras when an endpoint exposes its stages separatelyarray of objects
Output countnum_outputspositive integer
Encodingoutput_formatdocumented enum
Chat conversationmessagesarray
Output length / samplingmax_tokens / temperatureinteger / 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 object root 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:

KeyMeaning
widgetcontrol to render; values below
orderthe field order, descending first, unique within an endpoint
label / placeholderoverride the default label; input placeholder text
rowstextarea row count
step / unitnumeric step and unit suffix (s, px)
accept / max_size_mbMIME filter and per-file size cap for upload controls
advancedbelongs in a collapsed advanced section
primarymain input; pinned to the top and never collapsed
affects_pricechanging this value requires a fresh quote
visible_whenconditional visibility expression
enum_labelsenum 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
                  -> expired

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

On this page