Go SDK
The official Go SDK for SpicyAPI (github.com/spicyapi-ai/spicy-go) — install it, quote and run your first generation, upload a local file, walk your task history and handle every error code.
spicy-go is the official Go SDK for SpicyAPI. It puts image, video and audio generation behind one *spicy.Client: it attaches your API key, decodes the response envelope into typed structs, waits for a task with bounded back-off, uploads local files in one call and gives every failure a single error type you can match with errors.As.
It requires Go 1.23 or newer and uses the standard library only. That is deliberate: this package goes into your dependency graph, and every module it drags in is one more thing for you to audit.
Is this the right tool?
| You want to… | Use |
|---|---|
| Call SpicyAPI from your own Go code | This SDK |
| Call it from Node.js or TypeScript | The TypeScript SDK |
| Call it from Python, PHP or Java | The Python, 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 | An established Go client for the OpenAI, Anthropic or Gemini formats — see Text and streaming |
Install it
Go resolves modules straight from their repository, so there is no registry step and nothing to wait for. This command works today:
go get github.com/spicyapi-ai/spicy-goThen 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.
client, err := spicy.New(spicy.Options{}) // reads SPICY_API_KEY from the environment
if err != nil {
return err
}
request := spicy.CreateTaskInput{
Model: "MODEL_ID_FROM_CATALOG",
Input: map[string]any{"prompt": "a lantern in fog"},
}
quote, err := client.Quote(ctx, request)
if err != nil {
return err
}
log.Printf("this will cost at most %s", quote.MaxCharge)
// Store this next to your own record of the job before sending it: its whole
// purpose is to still be there after the crash that lost the response.
key, err := spicy.NewIdempotencyKey()
if err != nil {
return err
}
request.Model = quote.Model
request.QuoteID = quote.QuoteID
request.ExpectedCost = quote.EstimatedCost
task, err := client.CreateTask(ctx, request, key, spicy.CreateTaskOptions{})
if err != nil {
return err
}
final, err := client.WaitForTerminal(ctx, task.TaskID, spicy.WaitOptions{})
if err != nil {
return err
}
for _, asset := range final.ReadyAssets() {
log.Println(asset.URL)
}MODEL_ID_FROM_CATALOG is a placeholder. Take a real ID from the authenticated catalogue — client.ListModels, which is GET /api/v1/models — and never from memory or an old page: models are added, paused and retired. Build Input from that model's own inputSchema, which ListModels returns; every model accepts different fields.
Amounts are decimal USD strings, never floats, because binary floats cannot hold most decimal amounts exactly. There are no credits or points anywhere in this API.
A terminal state is not a successful one: failed, canceled and expired all end the wait. Check final.State.
Skip the first poll
WaitSeconds asks the service to hold the connection open until the task finishes, which on a quick model means the result arrives with the submission.
seconds := 30
task, err := client.CreateTask(ctx, request, key, spicy.CreateTaskOptions{WaitSeconds: &seconds})
if err != nil {
return err
}
// Task is nil whenever the wait ran out. That is the ordinary outcome, not an
// error — the generation is still running, so poll from here.
final := task.Task
if final == nil {
final, err = client.WaitForTerminal(ctx, task.TaskID, spicy.WaitOptions{})
if err != nil {
return err
}
}
log.Println(final.State, final.Cost)It is an optimisation, never a guarantee. Code that reads task.Task without the nil check works right up until the first generation slower than the budget, and then panics in production.
Start from a local file
Image-to-video, subject swap and image editing all need your material on our side first. UploadFile does the three-step upload — ticket, PUT to storage, commit — in one call and returns a spicy:// URI. That URI is what goes into Input.
uploaded, err := client.UploadFile(ctx, "/path/to/reference.png", "")
// uploaded.URI -> "spicy://f/fil_..."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.
Balance, usage and history
All three are scoped to the calling API key: sibling keys on the same account, and generations started in the web console, are not included, and their absence is not a fault.
balance, err := client.GetBalance(ctx)
if err != nil {
return err
}
log.Printf("%s available, %s held by accepted tasks", balance.Available, balance.Held)
usage, err := client.GetUsage(ctx, spicy.UsageOptions{From: "2026-09-01", To: "2026-09-08"})
if err != nil {
return err
}
log.Printf("%d calls, %s settled over [%s,%s)", usage.TotalCalls, usage.TotalSpend, usage.From, usage.To)
// EachTask repeats every filter on every page and moves only the cursor.
if err := client.EachTask(ctx, spicy.ListTasksOptions{From: "2026-09-01", To: "2026-09-08"},
func(task spicy.TaskListItem) bool {
log.Println(task.TaskID, task.State, task.Cost)
return true // return false to stop early
}); err != nil {
return err
}Dates are UTC calendar dates in a half-open interval [from, to) of at most 92 days. Pin From and To on any walk that can cross midnight UTC. Left empty, the server recomputes its own window — to is tomorrow in UTC — on every page, so a walk that starts at 23:59 and continues at 00:01 asks two different questions and splices the answers together. Nothing reports it.
GetUsage is for reconciliation, not for progress. It sits behind an account-wide budget shared by every key, and that limiter fails closed — polling it can lock every other key out of its own reporting. Follow a task with WaitForTerminal instead. Settlement can also land late, so a figure read today can still change tomorrow.
Errors
Failures come back as *spicy.APIError, reachable with errors.As:
var apiErr *spicy.APIError
if errors.As(err, &apiErr) {
log.Println(apiErr.Status, apiErr.Code, apiErr.RequestID, apiErr.RetryAfter)
}Branch on Code, never on the message — messages are translated, codes are not. HTTP 503 is shared by three different business codes, so the 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 RetryAfter |
50301 | 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 |
Every code is listed in Errors. Quote RequestID when you contact support.
Two things that will save you money
Keep one idempotency key per submission. Reuse it for every resend, including after a timeout. A lost response does not prove the task was not created — a fresh key turns an unknown outcome into a second paid task. CreateTask requires the key for exactly this reason. See Idempotency.
A task that succeeds is charged, even if the result disappoints. Quote reserves nothing, so quote first when the price matters.
What this package does not do
- Text models. They speak the OpenAI, Anthropic and Gemini wire formats — the established Go clients for those already work against
https://api.spicyapi.ai/v1with the same key. See Text and streaming. - Client-side applications. Never ship this key inside a binary you distribute. 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. Cancelling the
contextonly stops your waiting.
Source
The module is MIT-licensed and developed in the open at github.com/spicyapi-ai/spicy-go. 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
Python SDK
The same workflow from Python, with no runtime dependencies

