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 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"
}
FieldRule
modelExact, currently callable value from the authenticated live catalog
inputMust satisfy that same item's current inputSchema; unknown fields are rejected
callBackUrlOptional public HTTP(S) callback; v1 fixes this exact casing

Canonical fields

The same concept uses one snake_case name across all new endpoints:

ConceptFieldType
Positive promptpromptstring
Negative promptnegative_promptstring
Primary / final imageimage_url / last_image_urlURI or committed spicy:// URI
Primary video / audiovideo_url / audio_urlURI or committed spicy:// URI
Reference mediareference_image_urls / reference_video_urls / reference_audio_urlsURI array
Output durationduration_secondsnumber or integer
Resolution / ratioresolution / aspect_ratiodocumented enum
Native audiogenerate_audioboolean
Random seedseedinteger
Output countnum_outputspositive integer
Encodingoutput_formatdocumented 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 object root and default to additionalProperties: 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:

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.

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 ifthen: 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.

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.

On this page