Python SDK
The official Python SDK for SpicyAPI (spicyapi) — install it, quote and run your first generation, upload a local file, verify webhooks and read every error code.
spicyapi is the official Python SDK for SpicyAPI. It puts image, video and audio generation behind one client object: it attaches your API key, turns the response envelope into ordinary dictionaries, waits for a task to finish with sensible back-off, uploads local files in one call and verifies webhook signatures in constant time.
It requires Python 3.11 or newer and has no runtime dependencies. That is deliberate: this package goes into a dependency tree you already own, and every constraint it adds is one more chance of a conflict.
Is this the right tool?
| You want to… | Use |
|---|---|
| Call SpicyAPI from your own Python code | This SDK |
| Call it from Node.js or TypeScript | The TypeScript SDK |
| Call it from Go, PHP or Java | The Go, PHP or Java SDK |
| Generate things from a terminal without writing code | The CLI |
| Let an AI assistant call SpicyAPI for you | The MCP server and the Agent Skill |
| Use a chat or text model | The official openai, anthropic or google-genai library pointed at SpicyAPI — see Text and streaming |
Install it
pip install spicyapiThen put your key in the environment, so it never appears in your code or your shell history file:
export SPICY_API_KEY="sk-spicy-..."Your first result
Ask for the price, decide, create the task, wait, read the files. Quoting is free and reserves nothing.
import os, uuid
from spicyapi import SpicyClient, output_assets
client = SpicyClient() # reads SPICY_API_KEY from the environment
model = client.get_model("MODEL_ID_FROM_CATALOG")
quote = client.quote_task(model=model["model"], input_data={"prompt": "a lantern in fog"})
print(quote["estimatedCost"], quote["maxCharge"]) # decide before you spend
task = client.create_task(
model=model["model"],
input_data={"prompt": "a lantern in fog"},
idempotency_key=str(uuid.uuid4()),
quote_id=quote["quoteId"],
expected_cost=quote["estimatedCost"],
)
final = client.wait_for_terminal(task["taskId"])
for asset in output_assets(final): # module-level helper, not a method
print(asset["url"])MODEL_ID_FROM_CATALOG is a placeholder. Take a real ID from the authenticated catalogue — client.list_models(), which is GET /api/v1/models — and never from memory or an old page: models are added, paused and retired. Build input_data from that model's own inputSchema, which list_models(include_schema=True) returns; every model accepts different fields.
Amounts come back as decimal strings in US dollars, never floats, because binary floats cannot hold most decimal amounts exactly. Compare and add them with decimal.Decimal. There are no credits or points anywhere in this API.
wait_for_terminal returns on any terminal state. A terminal state is not a successful one — check final["state"] before reading output.
Some endpoints answer in text rather than files. Read output_text(final) first when the model produces a transcript or a caption; a client that only walks the asset list reports a perfectly successful task as having produced nothing.
Start from a local file
Image-to-video, subject swap and image editing all need your material on our side first. upload_file does the three-step upload — ticket, PUT to storage, commit — in one call and returns a spicy:// URI. That URI is what goes into the input.
uploaded = client.upload_file("/path/to/reference.png")
task = client.create_task(
model="MODEL_ID_FROM_CATALOG",
input_data={"image": uploaded["uri"], "prompt": "slow dolly in"},
idempotency_key=str(uuid.uuid4()),
)A publicly reachable HTTPS URL can go into the input directly and needs no upload at all. Images are accepted to 10 MiB, audio and video to 90 MiB; see Media for the rest.
Webhooks
verify_webhook is a module-level function, so a request handler can use it without building a client. It compares in constant time and checks the timestamp only after the signature is valid.
from spicyapi import verify_webhook
delivery = verify_webhook(
raw_body=request.body, # the exact bytes, before any parsing
signature=request.headers["X-Webhook-Signature"],
timestamp=request.headers["X-Webhook-Timestamp"],
payload_version=request.headers["X-Webhook-Payload-Version"],
secret=os.environ["SPICY_WEBHOOK_SECRET"],
)Verify the raw bytes. json.loads followed by json.dumps changes them, and the signature will never match again. Deliveries are retried, so deduplicate on the delivery ID. Full delivery format and retry schedule are in Webhooks.
Errors
Every failure raises SpicyApiError carrying status, code, request_id and retry_after_seconds. Branch on code, never on the message text — messages are translated, codes are not.
HTTP 503 is shared by three different business codes, so reading the HTTP status alone is not enough to decide what to do:
| code | Meaning | What to do |
|---|---|---|
40003 | uploaded bytes do not match their ticket | start over from a fresh ticket |
40004 | no deployment serves that parameter combination | change the parameter named in the message |
40201 | not enough balance | add funds |
40901 | the price moved before the task was created | quote again, keep the same idempotency key |
503 | a dependency is briefly unavailable | back off by Retry-After |
50301 | the model has no usable deployment or price right now | refresh the catalogue, do not hammer |
50302 | a synchronous generation failed upstream and was refunded | retry with a new idempotency key |
err.recovery returns the same guidance at runtime. Quote request_id when you contact support. Every code is listed in Errors.
Two things that will save you money
Keep one idempotency key per submission. Reuse it for every resend of that submission, including after a timeout or a dropped connection. A lost response does not prove the task was not created — a fresh key turns an unknown outcome into a second paid task. See Idempotency.
A task that succeeds is charged, even if the result disappoints. Quote first when the price matters; quote_task reserves nothing and creates nothing.
What this package does not do
- Text models. They speak the OpenAI, Anthropic and Gemini wire formats, so the official
openai,anthropicandgoogle-genailibraries already work — point them athttps://api.spicyapi.ai/v1with the same key. See Text and streaming. - Browser, mobile and desktop apps. Never ship this key inside an application: a key compiled into a client is a public key. Call from your server.
- Choosing a model or filling in its fields. It hands you the catalogue and each model's schema; you decide what to send.
- Cancelling a task. Once a task is accepted it runs to the end. Stopping your program only stops your waiting.
Source
The package is MIT-licensed and developed in the open at github.com/spicyapi-ai/spicy-python. Its README is the shortest complete tour of the client; this page adds the surrounding concepts.
Next steps
Model catalog and schemas
How to read a model's inputSchema and choose valid inputs
Asynchronous tasks
Task states, polling and what each field means
Quotes and compatibility
See the exact price of a request before you pay it
Webhooks
Delivery format, retry schedule and signature verification
Errors
Every error code and which ones are worth retrying
Go SDK
The same workflow from Go, on the standard library alone

