spicyapiDocs
Main content

Model catalog and schemas

Use the authenticated live catalog to select callable models, read their prices, and build requests from their current JSON Schema.

createTask.model takes the exact model returned by the authenticated GET /api/v1/models response. Its input is defined by that same item's current inputSchema. That runtime response is the only source of truth for a callable ID.

Do not copy an ID from a static page

Launch snapshots, family pages and model-creator catalogs are discovery surfaces. They may include planned, disabled or renamed entries and must never be used as a fallback when the live catalog is empty. Before paid work, fetch the authenticated catalog and require both enabled: true and available: true.

In the examples on this page, MODEL_ID_FROM_CATALOG is a placeholder, not a real model: replace it with a model value from your own live catalog. Reading the catalog is free.

Select a model and its validated input

Request schemas differ between models, so the safe runnable path selects a live model and one of its server-validated examples together:

const response = await fetch(
  'https://api.spicyapi.ai/api/v1/models?includeSchema=1&includeExamples=1',
  { headers: { Authorization: `Bearer ${process.env.SPICY_API_KEY}` } },
);
const envelope = await response.json();
if (!response.ok || envelope.code !== 200) throw new Error(envelope.msg);

const selected = envelope.data.items.find(
  (item) => item.enabled && item.available && item.examples?.length,
);
if (!selected) throw new Error('No callable model with a validated example');

const createTaskBody = {
  model: selected.model,
  input: selected.examples[0].input,
};

Each example is a complete input that the server has already validated against the current schema, so it can be submitted as it is.

Read the catalog programmatically

GET /api/v1/models
GET /api/v1/models/{model}
curl "https://api.spicyapi.ai/api/v1/models?modality=video" \
  -H "Authorization: Bearer $SPICY_API_KEY"

The list is not paginated: one response holds every model your key can see that matches the filters. All parameters are optional.

ParameterMeaning
modalityimage, video, audio or text
providerThe model creator, matched exactly and case-sensitively, for example ByteDance. Copy it from an item's provider
taskOne task, for example text-to-image, edit (image editing), image-to-video, text-to-video, reference-to-video, speech-to-text, video-analyze, video-edit, video-extend, character-animation or chat. Read tasks in the catalog for the current set. Image editing has two accepted spellings: models are published as .../edit, but tasks classifies them as image-to-image, and the filter takes either one
searchCase-insensitive text found anywhere in the model ID or display name, up to 128 characters
includeSchematrue or 1 adds each item's inputSchema
includeExamplestrue or 1 adds each item's examples, validated against the current schema

The single-model endpoint, GET /api/v1/models/{model}, always includes inputSchema and examples; includeSchema and includeExamples only matter on the list. Slashes in the ID can stay as they are; percent-encode them only if your HTTP client treats a path parameter as a single segment. A model that does not exist or is not currently offered returns 404.

A trimmed list response:

{
  "code": 200,
  "msg": "success",
  "data": {
    "total": 1,
    "items": [
      {
        "model": "MODEL_ID_FROM_CATALOG",
        "family": "…",
        "displayName": "…",
        "provider": "…",
        "modality": "video",
        "tasks": ["image-to-video"],
        "async": true,
        "enabled": true,
        "available": true,
        "availability": "available",
        "taskTimeoutSeconds": 1200,
        "maxOutputDurationSeconds": 10,
        "quantityField": "duration_seconds",
        "pricing": [
          { "variant": "generate_audio=false;resolution=720p", "unit": "per_second", "price": "0.05", "currency": "USD" },
          { "variant": "generate_audio=true;resolution=720p", "unit": "per_second", "price": "0.08", "currency": "USD" }
        ],
        "startingPrice": { "variant": "generate_audio=false;resolution=720p", "unit": "per_second", "price": "0.05", "currency": "USD" },
        "badges": ["audio_output"],
        "relatedModels": [],
        "version": "v1",
        "updatedAt": "2026-09-16T16:47:57Z"
      }
    ]
  },
  "request_id": "req_01k3m8x9q2z4v7n5p6r8s0t1w2"
}

The prices above are illustrative. inputSchema and examples appear in list items only when you ask for them.

Fields clients should consume

FieldPurpose
modelThe exact value to send as createTask.model, in the form publisher/model/task
familyStable product-family identifier for grouping related endpoints
displayNameHuman-readable name for this endpoint
providerThe model creator, such as the lab that built the model
modality / tasksThe medium this endpoint is grouped under in the catalog, and which task it performs. modality drives browsing and the modality filter; it is not a promise about the artefact type — a speech-to-text endpoint is grouped under audio and answers with text. See The shape of output
toolkitWhich family of tools this endpoint belongs to, currently only subject-swap. Absent when the model is in no toolkit, so test the value rather than the key. Like badges, it is a classification and never affects admission, price or validation. See Subject swap
asynctrue: create a task and collect the result by polling or webhook. false: the answer comes back in the response
inputSchemaJSON Schema 2020-12 used for form generation and request validation
examplesInputs validated against the current schema; on the list, returned only when requested
enabledWhether the entry can be called right now. The catalog also lists entries that are published but not yet open, and entries the platform has paused, and those come back false. Check it together with available before you send a task
availableWhether the model can be called right now. false means a call would fail, for example because it has no current price
availabilityOur stated phase: planned, available, preview (fields and outputs may still change) or maintenance (temporarily failing, expected back)
pricing / startingPriceYour prices; see Reading pricing
quantityFieldThe input field whose value is the billed quantity, commonly duration_seconds for video. Empty means each call counts as one unit. Always read it from the catalog
taskTimeoutSecondsThe platform's execution deadline for one task. It is not a media length
maxOutputDurationSecondsThe longest output the model can produce, taken from the maximum of its duration_seconds property; omitted when no maximum is declared
badgesCapability and licensing tags, such as commercial_use or no_watermark. Ignore values you do not recognise
relatedModelsHand-picked alternatives or next steps, as model IDs you can call
version / updatedAtChange tracking: compare them to tell whether a later read changed anything

mature and policyTier are informational labels. They play no part in whether a request is accepted.

Reading pricing

Each entry in pricing is one price tier:

FieldMeaning
unitper_image, per_second (of output media), per_request, or per_1k_tokens
priceYour price per unit in US dollars, as a decimal string. It already reflects your account's rates, which can differ between accounts
variantThe settings that select this tier. Empty when the model has a single price; the bare value (for example 720p) when one field selects the tier; field=value pairs joined by ; and sorted by field name (for example generate_audio=false;resolution=480p) when several do
currencyAlways USD
regularPrice, offerLabel, offerPercent, offerEndsAtPresent only while a customer offer applies: the price before the offer, the offer's name, its percentage and when it ends

startingPrice is the cheapest entry, not the price of your settings. The fields that select a tier are listed in inputSchema["x-pricing"].variantFields when present, and are otherwise the fields marked x-ui.affects_price: true (except quantityField). A field you omit counts at its schema default.

  • Media models: your cost is price × quantity, where the quantity is the value of quantityField in your input (or one unit when quantityField is empty). Some endpoints then add a separate charge for every extra input, most often one charge per reference image. Treat that as a real part of the bill, not a rounding detail: on an editing endpoint that accepts an array of reference images, filling the array can multiply the amount, while unit, quantity, variant and the tier price all stay exactly what they are for a single image — so the arithmetic above under-reports without any visible sign. The machine-readable signature is a field marked x-ui.affects_price: true that is neither the quantityField nor listed in x-pricing.variantFields: it moves the amount without selecting a tier. For any endpoint like that, read pricing as a floor and take the amount from a quote.
  • Text models (per_1k_tokens): one number cannot describe the charge. price is only the output-token rate per 1,000 tokens. Input and cached-input tokens have their own, usually lower, rates and are charged from the measured usage of each call. Do not multiply price by your total token count. The per-million-token prices for input, cached input and output are shown on each model's page and in the public catalogue's price.perMillionTokens.

For the amount of one exact request on the native API before paying, use POST /api/v1/jobs/quote. Final charges are in the task record (cost with settled: true) and in Billing.

Caching the catalog

  • The catalog response may be cached for up to 60 seconds (Cache-Control: private, max-age=60). Keep it in a private, per-account cache only: prices are specific to your account, so never put it behind a shared or public CDN cache.
  • Beyond that, do not re-read it before every task. Catalog reads use the account’s read allowance, independently of task submission. Fetching the catalog before every call still wastes read capacity; see Rate limits.
  • Refresh it on a timer (for example every few minutes), after a 400 validation error, and when a submission for that model is rejected.
  • Keep the version and updatedAt of the record you used, so you can tell whether a later read changed anything.
  • Never ship a hard-coded schema snapshot, and never fall back to a static page when the live catalog has no callable item.

Shared Schema and Playground rules

inputSchema uses JSON Schema 2020-12. Standard keywords control server validation; the property-level x-ui object only describes Playground ordering, presentation and quote-refresh hints. Clients must ignore unknown presentation extensions, which never replace required, enums, bounds or conditional branches. See the full Spicy Schema contract.

Render your own per-model parameter table

This site ships no per-model parameter table. That is not laziness: models and fields change every week, any static copy drifts, and the cost of reading a stale parameter is a 400 or a wrong price. Parameters have exactly one source — the inputSchema returned by the catalog. The Playground renders that same document, and the SDK validates against it.

What each part of a schema becomes in a table

In the schemaIn your parameter table
a key of propertiesthe parameter name. Field names are unified across models: one meaning, one name everywhere
membership in requiredrequired or optional
anyOf / allOfconditionally required. anyOf = supply at least one of these; allOf's if → then = if you supply A you must supply B
typethe type
descriptionthe help text. It is English prose and can be shown verbatim; you do not need to write your own
enumaccepted values. When x-ui.enum_labels is present, use it to turn values into human labels
minimum / maximumthe accepted range; x-ui.step is the step and x-ui.unit the suffix (s, px)
minItems / maxItemsfor an array, how many entries it accepts. A row that leaves this out tells the reader nothing about the limit they will hit
itemsfor an array, the shape of one entry. When items.type is object, that subschema is a complete object schema in its own right: read its required, its additionalProperties, and the type, description, range and default of each sub-field, and render them as a nested table rather than a free-text box
defaultthe default applied when you omit the field
x-ui.orderthe sort key, descending first. Do not sort by the key order of properties — JSON object key order is not part of the contract
x-ui.primarypin to the top as a main input
x-ui.advancedsafe to collapse into an advanced section
x-ui.affects_pricemark it as price-affecting; changing the value warrants a fresh quote
x-ui.widgetwhich control to render when you build an editable form (thirteen values, see Spicy Schema)

Conditional branches appear only at the root of inputSchema, in two shapes: anyOf, where each branch lists only required fields ("supply at least one of these"), and allOf, where each branch is if → then ("if you supply A you must supply B"). Do not infer them from the family, and do not expect oneOf, not or dependentSchemas. New endpoints follow Spicy Schema canonical fields; older endpoints follow their live schema.

Fetching it

const modelId = 'MODEL_ID_FROM_CATALOG'; // the exact `model` value from GET /api/v1/models
const res = await fetch(`https://api.spicyapi.ai/api/v1/models/${modelId}`, {
  headers: { Authorization: `Bearer ${process.env.SPICY_API_KEY}` },
});
const { data } = await res.json();
const schema = data.inputSchema;

const rows = Object.entries(schema.properties)
  .sort(([, a], [, b]) => (b['x-ui']?.order ?? 0) - (a['x-ui']?.order ?? 0))
  .map(([name, prop]) => ({
    name,
    required: (schema.required ?? []).includes(name),
    type: prop.type,
    description: prop.description,
    values: prop.enum
      ? prop.enum.map((v) => prop['x-ui']?.enum_labels?.[v] ?? v)
      : prop.minimum !== undefined || prop.maximum !== undefined
        ? [`${prop.minimum ?? ''}–${prop.maximum ?? ''}${prop['x-ui']?.unit ?? ''}`]
        : [],
    default: prop.default,
    affectsPrice: prop['x-ui']?.affects_price === true,
    advanced: prop['x-ui']?.advanced === true,
  }));

That sketch deliberately stops at scalars: it leaves values empty for an array, because an array says what it accepts in items, minItems and maxItems rather than in enum or a range. Extend it before you ship it — a row that shows an array parameter as a bare array with no entry shape and no length limit is the one row a reader cannot act on.

The single-model record also returns examples: each one is a complete input already validated server-side against the current schema, so it works both as a minimal runnable example in your docs and as a request body you can submit as-is. That is safer than composing one by hand, because a hand-written example goes stale when the schema changes and examples does not.

The same code covers any model by changing modelId, and it does not expire when we add new models. To generate a table for the whole catalog, loop over items from GET /api/v1/models?includeSchema=1, and cache it as described in Caching the catalog.

Longer help text than description

description is one sentence, sized for a form label. Where a parameter needs more than that, the public catalogue carries a second note per parameter:

curl "https://api.spicyapi.ai/console/v1/catalog/models/MODEL_ID_FROM_CATALOG?locale=en"

The single-model record returns paramNotes, an object keyed by parameter name, written in any of the ten documentation languages via locale. It is where the things that do not fit a label live — what a value is actually measured against, which setting makes a call noticeably slower, and which inputs are accepted by the schema yet still lead to a disappointing result. For a parameter that points at a file you host yourself, it is usually the only place that says whether we fetch it, when we fetch it and what happens if it turns out to be unusable.

Two boundaries: paramNotes is on the public catalogue, which needs no key, and not on the authenticated GET /api/v1/models, so read it separately and cache it beside the schema. And it is prose, never a rule — inputSchema remains the only thing that decides whether a request is accepted.

On this page