spicyapiDocumentación
SDK
Contenido principal

SDK de Java

El SDK oficial de Java para SpicyAPI (ai.spicyapi spicyapi-java) — instálalo, cotiza y lanza tu primera generación, sube un archivo local, verifica las llamadas de retorno y recorre de forma exhaustiva una taxonomía de errores sellada.

spicyapi-java es el SDK oficial de Java para SpicyAPI. Pone la generación de imagen, video y audio detrás de un único SpicyClient: adjunta tu clave de API, convierte la envoltura de la respuesta en records, espera a una tarea con esperas crecientes acotadas, sube archivos locales en una sola llamada, verifica las firmas de las llamadas de retorno y convierte cada fallo en un tipo de excepción sellado, de modo que un switch sobre la taxonomía es exhaustivo en tiempo de compilación.

Necesita Java 17 o posterior y tiene una única dependencia: Jackson (com.fasterxml.jackson.core:jackson-databind), 2.12 o posterior, que es la versión en la que los records pasaron a ser mapeables, y aquí los tipos son records. Jackson es una dependencia de compilación normal, ni optional ni empaquetada dentro, así que gana la versión que tu proyecto ya tenga. En la ruta de módulos, el cliente es ai.spicyapi, fijado en el manifiesto en lugar de deducirse del nombre del archivo JAR.

¿Es la herramienta adecuada?

Quieres…Usa
Llamar a SpicyAPI desde tu propio código de Java o de la JVMEste SDK
Llamarla desde Node.js o TypeScriptEl SDK de TypeScript
Llamarla desde Python, Go o PHPEl SDK de Python, de Go o de PHP
Generar cosas desde una terminal, sin escribir códigoLa CLI
Que un asistente de IA llame a SpicyAPI por tiEl servidor MCP y el Agent Skill
Usar un modelo de chat o de textoUn cliente de Java consolidado para los formatos de OpenAI, Anthropic o Gemini; consulta Texto y streaming

Instálalo

Añade la dependencia desde Maven Central:

<dependency>
  <groupId>ai.spicyapi</groupId>
  <artifactId>spicyapi-java</artifactId>
  <version>0.1.0</version>
</dependency>

O, con Gradle (Kotlin DSL):

implementation("ai.spicyapi:spicyapi-java:0.1.0")

Guarda tu clave en el entorno, para que no aparezca nunca ni en tu código ni en el control de versiones:

export SPICY_API_KEY="sk-spicy-..."

Tu primer resultado

Pide el precio, decide, crea la tarea, espera y lee la salida. Cotizar es gratis y no retiene nada. run es createTask seguido de 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);              // gratis; no retiene 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);     // algunos endpoints responden con texto, no con archivos
for (TaskAsset asset : task.assets()) {
    asset.url().ifPresent(System.out::println);
}

MODEL_ID_FROM_CATALOG es un marcador de posición. Saca un ID real del catálogo autenticado —client.listModels(), que es GET /api/v1/models— y nunca de memoria ni de una página antigua: los modelos se añaden, se pausan y se retiran. Construye la entrada a partir del inputSchema del propio modelo, que devuelve listModels(ModelFilter.all().withIncludeSchema(true)); cada modelo acepta campos distintos.

Comprueba outputText() antes de recorrer assets(). La transcripción y otros endpoints parecidos ponen todo el resultado en el campo de texto y no devuelven ningún archivo; un cliente que solo busca archivos da por vacía una tarea que ha salido perfectamente.

Los importes son cadenas decimales en dólares estadounidenses, nunca números en coma flotante, porque un flotante binario no puede guardar con exactitud la mayoría de los importes decimales. En esta API no hay créditos ni puntos por ninguna parte.

Empieza desde un archivo local

La generación de imagen a video, la sustitución de personas y la edición de imágenes necesitan que tu material esté antes de nuestro lado. uploadFile hace los tres pasos en una sola llamada y devuelve un URI spicy://. Ese URI es lo que va en la entrada. Una URL HTTPS accesible públicamente puede ir directamente y no necesita ninguna subida.

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());

Los tres pasos también son públicos, para bytes que lleguen en streaming desde otro sitio:

UploadTicket ticket = client.createUploadUrl(UploadContentType.IMAGE_PNG, data.length);
client.putUploadBytes(ticket, data);                  // va al almacenamiento de objetos, no a la API
UploadedFile file = client.commitUploadedFile(ticket.fileId());

Hay dos cosas del paso intermedio que fallan de forma opaca si lo reimplementas. Las cabeceras del ticket forman parte de lo que firmó la URL prefirmada, así que se reenvían sin tocar: si quitas una, le cambias el nombre o añades otra por ser servicial, el almacenamiento responde 403. Y no se adjunta ninguna clave de SpicyAPI: la propia URL es el permiso, y apunta al almacenamiento, no a nosotros.

Se aceptan imágenes de hasta 10 MiB, y audio y video de hasta 90 MiB. El cliente rechaza en local un archivo demasiado grande, antes incluso de pedir el ticket. Consulta Carga y descarga de medios.

Conserva el resultado menos tiempo

Las salidas y el texto de solicitud almacenado se conservan durante lo que tenga configurado la cuenta. Una tarea concreta puede pedir menos:

TaskRecord task = client.run(request, idempotencyKey, Duration.ofHours(1));
task.retention().outputsExpireAt();                   // lo que se aplicó realmente

Solo acorta: el servicio se queda con el menor de tres valores —el tuyo, el ajuste de la cuenta y el máximo de la plataforma—, así que una solicitud nunca puede comprar una ventana más larga de la que permite la cuenta. Duration.ZERO es un valor real: las salidas desaparecen en cuanto la tarea llega a un estado terminal. Un valor demasiado grande se recorta en lugar de rechazarse, y por eso lo que se aplicó es retention() en el record, no el número que enviaste. retryTask no lleva conservación propia: un reintento hereda la de su tarea de origen. Consulta Conservación y destrucción.

Saldo, consumo e historial

Balance balance = client.getBalance();                          // toda la cuenta
UsageReport usage = client.getUsage(                            // solo esta clave
        LocalDate.of(2026, 9, 13), LocalDate.of(2026, 9, 20));  // [from, to)
TaskPage page = client.listTasks(TaskFilter.all().withState(TaskState.FAILED));

El saldo es un indicador de presupuesto, no una decisión de admisión. Una parte puede ser dinero de bonificación acotado a determinados modelos: balance.fundingSources() es el desglose, y un modelSlugs vacío en una bonificación significa todos los modelos, no ninguno. La respuesta exacta para una solicitud exacta es quote(), que aplica las mismas reglas de admisión y no retiene nada.

El consumo sirve para cuadrar cuentas, no para seguir el progreso. Solo cuenta los cargos ya liquidados, así que un periodo todavía en marcha siempre se queda corto; una liquidación tardía puede cambiar un día pasado; y lleva su propio límite de solicitudes para toda la cuenta cuyo limitador falla en cerrado, de modo que sondearlo puede dejar a todas las claves de la cuenta sin acceso a sus propios informes. Para seguir una tarea usa waitForTerminal.

listTasks se limita a la clave que hace la llamada y devuelve solo metadatos. Recorre las páginas moviendo el cursor y sin cambiar nada más, y fija from y to antes de empezar: si los dejas sin poner, el servicio recalcula su ventana predeterminada (to es mañana en UTC) en cada solicitud, así que un recorrido que cruza la medianoche hace dos preguntas distintas y empalma las respuestas. Nada te avisa de ello.

Llamadas de retorno

Pon callBackUrl en la solicitud y el record terminal se te entrega. Verifícalo antes de creértelo:

CallbackSignatureVerifier verifier =
        new CallbackSignatureVerifier(System.getenv("SPICY_WEBHOOK_SECRET"));

byte[] body = request.getInputStream().readAllBytes();   // los bytes en bruto, nunca reserializados
WebhookDelivery delivery = verifier.verify(body, request::getHeader);
delivery.task().ifPresent(record -> { /* el mismo record que devuelve getTask */ });

Tres formas en las que esto sale mal, y que la clase evita todas: volver a serializar el cuerpo antes de verificarlo (un readValue seguido de un writeValueAsString cambia los bytes, y el resumen ya no vuelve a coincidir), comparar firmas con equals (devuelve el control en el primer byte distinto, lo que es una diferencia de tiempo medible) y saltarse la comprobación de frescura (una firma válida sigue siendo válida para siempre, así que una entrega capturada se puede reproducir más tarde).

Las entregas se reintentan. Deduplica por delivery.deliveryId(). Consulta Webhooks.

Errores

Cada fallo es una SpicyException, que está sellada, así que un switch sobre la taxonomía es exhaustivo:

TipoQué significa
SpicyApiExceptionel servicio respondió, y la respuesta fue que no
SpicyProtocolExceptionalgo respondió, pero no con nuestra envoltura: un proxy, un portal cautivo, una URL base equivocada
SpicyTransportExceptionno hubo ninguna respuesta utilizable: DNS, TLS, una conexión cortada
SpicyTimeoutExceptionse agotó un plazo local de la solicitud. Desenlace desconocido, no «no ocurrió»
SpicyWaitTimeoutExceptionel sondeo se dio por vencido. La tarea sigue en marcha y se sigue pudiendo cobrar; la excepción lleva su ID
SpicyUploadExceptionel almacenamiento de objetos rechazó el PUT prefirmado
SpicyWebhookExceptionuna entrega entrante no era auténtica, reciente y correcta

Ramifica según businessCode(), nunca según el mensaje: los mensajes son prosa, se traducen y no forman parte del contrato. El estado HTTP 503 lo comparten tres códigos de negocio distintos, así que el estado por sí solo no te dice qué hacer:

CodeQué significaQué hacer
40003los bytes subidos no coinciden con su ticketempieza de cero con un ticket nuevo; volver a confirmar no lo arregla
40004ningún despliegue atiende esa combinación de parámetroscambia el parámetro que nombra el mensaje
40201saldo insuficienteañade fondos
40901el precio cambió antes de que se creara la tareavuelve a cotizar y conserva la misma clave de idempotencia
503una dependencia no está disponible durante un momentoespera lo que indique Retry-After
50301ahora mismo no hay ningún despliegue ni precio utilizableespera un poco, o elige otro modelo
50302una generación síncrona falló aguas arriba y se reembolsóreintenta con una clave de idempotencia nueva; la original repite el fallo

remediation() devuelve ese mismo consejo en tiempo de ejecución, y describe() deja en una sola línea el estado, el código, el ID de solicitud, el mensaje y el consejo, lista para un registro. Cita el requestId() cuando escribas a soporte. Todos los códigos están en Errores.

Dos cosas que te ahorrarán dinero

Usa una sola clave de idempotencia por envío. Reutilízala en cada reenvío, también después de un tiempo de espera agotado. Que se pierda una respuesta no demuestra que la tarea no se haya creado: una clave nueva convierte un resultado desconocido en una segunda tarea de pago. Por eso mismo createTask reintenta automáticamente solo si le pasaste una clave: sin ella, un reintento tras un tiempo de espera agotado sería una segunda generación y un segundo cobro. Consulta Idempotencia.

Una tarea que sale bien se cobra, aunque el resultado te decepcione. quote() no retiene nada y no crea nada, así que cotiza primero cuando el precio importe. createTask retiene el estimatedCost, y esa retención es el techo: lo que no se use se devuelve, y nada por encima de esa cifra se cobra después.

Lo que este paquete no hace

  • Modelos de texto. Hablan los formatos de OpenAI, Anthropic y Gemini: los clientes de Java consolidados para esos formatos ya funcionan contra https://api.spicyapi.ai/v1 con la misma clave. Consulta Texto y streaming.
  • Aplicaciones de cliente. No metas nunca esta clave dentro de una aplicación de Android, de un binario de escritorio ni de nada que una persona pueda abrir. Llama desde tu servidor.
  • Elegir un modelo o rellenar sus campos. Te entrega el catálogo y el esquema de cada modelo; tú decides qué enviar.
  • Cancelar una tarea. Cuando una tarea se acepta, se ejecuta hasta el final.

Código fuente

El artefacto tiene licencia MIT y se desarrolla en abierto en github.com/spicyapi-ai/spicy-java. mvn -B verify ejecuta la batería de pruebas unitarias contra un stub local, más una comprobación de deriva del contrato que compara el contrato incluido en el paquete con el publicado en https://docs.spicyapi.ai/openapi.yaml y después vuelve a revisar cada enumeración que el paquete lleva escrita a mano, porque si el contrato gana un valor nuevo y el cliente no lo gana con él, eso no falla en ninguna parte: el cliente se limita a rechazar el valor nuevo en local, y en silencio.

Siguientes pasos

En esta página