SDK para Java
O SDK oficial da SpicyAPI para Java (ai.spicyapi spicyapi-java) — instale, cote e faça a sua primeira geração, envie um arquivo local, verifique callbacks e use um switch exaustivo sobre uma taxonomia de erros selada.
O spicyapi-java é o SDK oficial para Java da SpicyAPI. Ele coloca a geração de imagem, vídeo e áudio atrás de um único SpicyClient: anexa a sua chave de API, mapeia o envelope da resposta em records, espera por uma tarefa com pausas crescentes e limitadas, envia arquivos locais numa única chamada, verifica assinaturas de callback e transforma toda falha num tipo de exceção selado — de modo que um switch sobre a taxonomia é exaustivo já em tempo de compilação.
Ele exige Java 17 ou mais recente e tem uma dependência: o Jackson (com.fasterxml.jackson.core:jackson-databind), 2.12 ou mais recente — essa é a versão em que records passaram a ser mapeáveis, e os tipos aqui são records. O Jackson é uma dependência de compilação comum, nem optional nem embutida, então prevalece a versão que o seu projeto já usa. No module path, o cliente é ai.spicyapi, fixado no manifesto em vez de derivado do nome do arquivo JAR.
Esta é a ferramenta certa?
| Você quer… | Use |
|---|---|
| Chamar a SpicyAPI a partir do seu próprio código Java ou JVM | Este SDK |
| Chamá-la a partir de Node.js ou TypeScript | O SDK para TypeScript |
| Chamá-la a partir de Python, Go ou PHP | O SDK para Python, Go ou PHP |
| Gerar conteúdo pelo terminal, sem escrever código | A CLI |
| Deixar um assistente de IA chamar a SpicyAPI por você | O servidor MCP e o Agent Skill |
| Usar um modelo de conversa ou de texto | Um cliente Java consolidado para os formatos da OpenAI, da Anthropic ou do Gemini — veja Texto e streaming |
Instalar
Adicione a dependência a partir do Maven Central:
<dependency>
<groupId>ai.spicyapi</groupId>
<artifactId>spicyapi-java</artifactId>
<version>0.1.0</version>
</dependency>Ou, com Gradle (Kotlin DSL):
implementation("ai.spicyapi:spicyapi-java:0.1.0")Coloque a sua chave no ambiente, para que ela nunca apareça no seu código nem no controle de versão:
export SPICY_API_KEY="sk-spicy-..."Seu primeiro resultado
Peça o preço, decida, crie a tarefa, espere, leia a saída. Cotar é grátis e não reserva nada. O run é o createTask seguido do waitForTerminal.
SpicyClient client = SpicyClient.builder().apiKeyFromEnvironment().build(); // SPICY_API_KEY
CreateTaskRequest request = CreateTaskRequest.of(
"MODEL_ID_FROM_CATALOG",
Map.of("prompt", "a lantern in fog"));
TaskQuote quote = client.quote(request); // grátis; não reserva nada
System.out.println("this will cost at most " + quote.maxCharge());
TaskRecord task = client.run( // createTask + waitForTerminal
request.withQuote(quote), UUID.randomUUID().toString());
task.outputText().ifPresent(System.out::println); // alguns endpoints respondem com texto, não com arquivos
for (TaskAsset asset : task.assets()) {
asset.url().ifPresent(System.out::println);
}MODEL_ID_FROM_CATALOG é um marcador. Pegue um ID de verdade no catálogo autenticado — client.listModels(), que é GET /api/v1/models — e nunca de memória nem de uma página antiga: modelos são acrescentados, pausados e aposentados. Monte a entrada a partir do inputSchema daquele modelo, que listModels(ModelFilter.all().withIncludeSchema(true)) devolve; cada modelo aceita campos diferentes.
Confira o outputText() antes de percorrer os assets(). Transcrição e endpoints parecidos colocam o resultado inteiro no campo de texto e não devolvem arquivo nenhum; um cliente que só procura arquivos informa que uma tarefa perfeitamente bem-sucedida não produziu nada.
Os valores são texto decimal em dólares americanos, nunca floats, porque números binários de ponto flutuante não guardam a maioria dos valores decimais com exatidão. Não existem créditos nem pontos em lugar nenhum desta API.
Começar por um arquivo local
Imagem para vídeo, troca de personagem e edição de imagem precisam do seu material do nosso lado antes de mais nada. O uploadFile faz as três etapas numa única chamada e devolve uma URI spicy://. É essa URI que vai na entrada. Uma URL HTTPS acessível publicamente pode ir direto e não precisa de envio nenhum.
UploadedFile uploaded = client.uploadFile(Path.of("/path/to/reference.png"));
// uploaded.uri() -> "spicy://f/fil_..."
TaskRecord task = client.run(
CreateTaskRequest.of("MODEL_ID_FROM_CATALOG",
Map.of("image", uploaded.uri(), "prompt", "slow dolly in")),
UUID.randomUUID().toString());As três etapas também são públicas, para bytes que venham de outro lugar em streaming:
UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data); // vai para o armazenamento de objetos, não para a API
UploadedFile file = client.commitUploadedFile(ticket.fileId());Duas coisas da etapa do meio falham de um jeito obscuro se você a reimplementar. Os cabeçalhos do ticket fazem parte do que a URL pré-assinada assinou, então eles são repassados sem alteração: derrube um, renomeie um ou acrescente um "para ajudar" e o armazenamento responde 403. E nenhuma chave da SpicyAPI é anexada: a própria URL é a autorização, e ela aponta para o armazenamento, não para nós.
Imagens são aceitas até 10 MiB; áudio e vídeo, até 90 MiB. O cliente recusa localmente um arquivo grande demais, antes mesmo de pedir o ticket. Veja Mídia.
Guardar o resultado por menos tempo
As saídas e o texto de requisição armazenado ficam guardados pelo tempo configurado na conta. Uma tarefa pode pedir menos:
TaskRecord task = client.run(request, idempotencyKey, Duration.ofHours(1));
task.retention().outputsExpireAt(); // o que de fato foi aplicadoIsso só encurta: o serviço fica com o menor entre o seu valor, a configuração da conta e o máximo da plataforma, então uma requisição nunca consegue comprar de volta uma janela maior do que a conta permite. Duration.ZERO é um valor de verdade — as saídas somem assim que a tarefa chega a um estado terminal. Um valor grande demais é limitado, não recusado, e é por isso que o que vale é o retention() do record, não o número que você enviou. O retryTask não tem retenção própria: uma nova tentativa herda a da tarefa de origem. Veja Retenção e destruição.
Saldo, uso e histórico
Balance balance = client.getBalance(); // da conta inteira
UsageReport usage = client.getUsage( // só desta chave
LocalDate.of(2026, 9, 13), LocalDate.of(2026, 9, 20)); // [from, to)
TaskPage page = client.listTasks(TaskFilter.all().withState(TaskState.FAILED));O saldo é um indicador de orçamento, não uma decisão de admissão. Parte dele pode ser bônus restrito a modelos específicos — balance.fundingSources() traz essa divisão, e um modelSlugs vazio num bônus significa todos os modelos, não nenhum. A resposta exata para uma requisição exata é o quote(), que roda as mesmas regras de admissão e não reserva nada.
O uso serve para conferência contábil, não para acompanhar o andamento. Ele conta apenas cobranças já liquidadas, então um período ainda em curso sempre aparece abaixo do real; a liquidação atrasada pode mudar um dia passado; e ele carrega o próprio limite de requisições, válido para a conta inteira, cujo limitador falha fechando — ficar consultando pode trancar todas as chaves da conta para fora dos próprios relatórios. Acompanhe uma tarefa com waitForTerminal.
O listTasks é limitado à chave que fez a chamada e devolve apenas metadados. Percorra as páginas movendo o cursor e sem mudar mais nada, e fixe from e to antes de começar: deixados em branco, o serviço recalcula a janela padrão dele (to é o dia seguinte em UTC) a cada requisição, então uma varredura que atravessa a meia-noite faz duas perguntas diferentes e emenda as respostas. Nada avisa isso.
Callbacks
Defina o callBackUrl na requisição e o record final é entregue a você. Verifique antes de acreditar nele:
CallbackSignatureVerifier verifier =
new CallbackSignatureVerifier(System.getenv("SPICY_WEBHOOK_SECRET"));
byte[] body = request.getInputStream().readAllBytes(); // os bytes crus, nunca reserializados
WebhookDelivery delivery = verifier.verify(body, request::getHeader);
delivery.task().ifPresent(record -> { /* o mesmo record que o getTask devolve */ });Três jeitos de isso dar errado, todos evitados por esta classe: reserializar o corpo antes de verificar (um readValue seguido de writeValueAsString muda os bytes, e o digest nunca mais bate), comparar assinaturas com equals (ele retorna no primeiro byte diferente, o que é uma diferença de tempo mensurável) e pular a conferência de validade (uma assinatura válida continua válida para sempre, então uma entrega capturada pode ser reenviada por terceiros).
As entregas são repetidas. Elimine duplicatas pelo delivery.deliveryId(). Veja Webhooks.
Erros
Toda falha é uma SpicyException, que é selada, então um switch sobre a taxonomia é exaustivo:
| Tipo | O que significa |
|---|---|
SpicyApiException | o serviço respondeu, e a resposta foi não |
SpicyProtocolException | alguma coisa respondeu, mas não com o nosso envelope — um proxy, um portal cativo, uma URL base errada |
SpicyTransportException | nenhuma resposta utilizável: DNS, TLS, uma conexão derrubada |
SpicyTimeoutException | um orçamento local de tempo se esgotou. Resultado desconhecido, e não "não aconteceu" |
SpicyWaitTimeoutException | a consulta desistiu. A tarefa continua rodando e continua cobrável; a exceção carrega o ID dela |
SpicyUploadException | o armazenamento de objetos recusou o PUT pré-assinado |
SpicyWebhookException | uma entrega recebida não era autêntica, recente e bem formada |
Decida pelo businessCode(), nunca pela mensagem — mensagens são texto corrido, são traduzidas e não fazem parte do contrato. O HTTP 503 é compartilhado por três códigos de negócio diferentes, então o status sozinho não diz o que fazer:
| Código | O que significa | O que fazer |
|---|---|---|
40003 | os bytes enviados não correspondem ao ticket deles | recomece com um ticket novo; fazer o commit de novo não resolve |
40004 | nenhuma implantação atende a essa combinação de parâmetros | mude o parâmetro citado na mensagem |
40201 | saldo insuficiente | adicione saldo |
40901 | o preço mudou antes de a tarefa ser criada | cote de novo, mantendo a mesma chave de idempotência |
503 | uma dependência está indisponível por pouco tempo | espere o que o Retry-After pedir |
50301 | nenhuma implantação nem preço utilizáveis neste momento | espere um pouco ou escolha outro modelo |
50302 | uma geração síncrona falhou no processamento e foi estornada | tente de novo com uma chave de idempotência nova; a original repete a falha |
O remediation() devolve o mesmo conselho em tempo de execução, e o describe() coloca status, código, ID da requisição, mensagem e conselho numa única linha, pronta para o log. Cite o requestId() ao falar com o suporte. Todos os códigos estão listados em Erros.
Duas coisas que vão economizar o seu dinheiro
Use uma chave de idempotência por envio. Reaproveite-a em cada reenvio, inclusive depois de um tempo esgotado. Uma resposta perdida não prova que a tarefa não foi criada — uma chave nova transforma um resultado desconhecido numa segunda tarefa paga. É também por isso que o createTask repete a chamada automaticamente apenas quando você passou uma chave: sem ela, uma nova tentativa depois de um tempo esgotado seria uma segunda geração e uma segunda cobrança. Veja Idempotência.
Uma tarefa que dá certo é cobrada, mesmo que o resultado decepcione. O quote() não reserva nada e não cria nada, então cote antes quando o preço importar. O createTask reserva o estimatedCost, e essa reserva é o teto: o que não for usado volta, e nada acima dela é cobrado depois.
O que este pacote não faz
- Modelos de texto. Eles falam os formatos de rede da OpenAI, da Anthropic e do Gemini — os clientes Java consolidados para esses formatos já funcionam contra
https://api.spicyapi.ai/v1com a mesma chave. Veja Texto e streaming. - Aplicações que rodam no cliente. Nunca distribua esta chave dentro de um aplicativo Android, de um binário de desktop ou de qualquer outra coisa que um usuário consiga abrir. Chame a partir do seu servidor.
- Escolher o modelo ou preencher os campos dele. O pacote entrega o catálogo e o schema de cada modelo; quem decide o que enviar é você.
- Cancelar uma tarefa. Depois de aceita, ela vai até o fim.
Código-fonte
O artefato tem licença MIT e é desenvolvido abertamente em github.com/spicyapi-ai/spicy-java. O mvn -B verify roda a suíte de testes unitários contra um stub local, mais uma verificação de desvio de contrato que compara o contrato embarcado com o publicado em https://docs.spicyapi.ai/openapi.yaml e depois reconfere cada enum que o pacote traz fixo no código — porque um contrato que ganha um valor novo sem o cliente ganhar junto não falha em lugar nenhum: ele simplesmente recusa o valor novo, localmente e em silêncio.
Próximos passos
Catálogo de modelos e schemas
Como ler o inputSchema de um modelo e escolher entradas válidas
Tarefas assíncronas
Estados da tarefa, polling e o que significa cada campo
Cotações e compatibilidade
Veja o preço exato de uma requisição antes de pagá-lo
Webhooks
Formato das entregas, calendário de novas tentativas e verificação de assinatura
Erros
Todos os códigos de erro e quais valem uma nova tentativa
SDK para PHP
O mesmo fluxo em PHP, sem nenhuma dependência de biblioteca
SDK para PHP
O SDK oficial da SpicyAPI para PHP (spicyapi/spicyapi) — instale com o Composer, cote e faça a sua primeira geração, envie um arquivo local, verifique webhooks com segurança e trate todos os códigos de erro.
Agentes e automação
Qual ferramenta oficial usar e um roteiro de produção para agentes de IA, workers de backend e pipelines de mídia repetíveis.

