spicyapiDocumentação
SDKs
Conteúdo principal

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 JVMEste SDK
Chamá-la a partir de Node.js ou TypeScriptO SDK para TypeScript
Chamá-la a partir de Python, Go ou PHPO SDK para Python, Go ou PHP
Gerar conteúdo pelo terminal, sem escrever códigoA 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 textoUm 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 aplicado

Isso 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:

TipoO que significa
SpicyApiExceptiono serviço respondeu, e a resposta foi não
SpicyProtocolExceptionalguma coisa respondeu, mas não com o nosso envelope — um proxy, um portal cativo, uma URL base errada
SpicyTransportExceptionnenhuma resposta utilizável: DNS, TLS, uma conexão derrubada
SpicyTimeoutExceptionum orçamento local de tempo se esgotou. Resultado desconhecido, e não "não aconteceu"
SpicyWaitTimeoutExceptiona consulta desistiu. A tarefa continua rodando e continua cobrável; a exceção carrega o ID dela
SpicyUploadExceptiono armazenamento de objetos recusou o PUT pré-assinado
SpicyWebhookExceptionuma 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ódigoO que significaO que fazer
40003os bytes enviados não correspondem ao ticket delesrecomece com um ticket novo; fazer o commit de novo não resolve
40004nenhuma implantação atende a essa combinação de parâmetrosmude o parâmetro citado na mensagem
40201saldo insuficienteadicione saldo
40901o preço mudou antes de a tarefa ser criadacote de novo, mantendo a mesma chave de idempotência
503uma dependência está indisponível por pouco tempoespere o que o Retry-After pedir
50301nenhuma implantação nem preço utilizáveis neste momentoespere um pouco ou escolha outro modelo
50302uma geração síncrona falhou no processamento e foi estornadatente 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/v1 com 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

Nesta página