spicyapiDocumentación
SDK
Contenido principal

SDK de PHP

El SDK oficial de PHP para SpicyAPI (spicyapi/spicyapi) — instálalo con Composer, cotiza y lanza tu primera generación, sube un archivo local, verifica webhooks sin riesgos y trata todos los códigos de error.

spicyapi/spicyapi es el SDK oficial de PHP para SpicyAPI. Pone la generación de imagen, video y audio detrás de un único SpicyClient: adjunta tu clave de API, desenvuelve la envoltura de la respuesta en arrays asociativos normales, espera a una tarea con esperas crecientes acotadas, sube archivos locales en una sola llamada y verifica las firmas de los webhooks sin cometer ninguno de los tres errores en los que el código PHP suele caer ahí.

Necesita PHP 8.1 o posterior, más ext-json y ext-hash, y no tiene ninguna dependencia de bibliotecas. Usa ext-curl cuando está presente; sin ella, el cliente recurre al envoltorio de flujo https://.

¿Es la herramienta adecuada?

Quieres…Usa
Llamar a SpicyAPI desde tu propio código de PHPEste SDK
Llamarla desde Node.js o TypeScriptEl SDK de TypeScript
Llamarla desde Python, Go o JavaEl SDK de Python, de Go o de Java
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 PHP consolidado para los formatos de OpenAI, Anthropic o Gemini; consulta Texto y streaming

Instálalo

composer require spicyapi/spicyapi

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 los archivos. Cotizar es gratis y no retiene nada.

use SpicyApi\SpicyClient;

$client = new SpicyClient();                 // lee SPICY_API_KEY del entorno

$quote = $client->quote('MODEL_ID_FROM_CATALOG', ['prompt' => 'a lantern in fog']);
printf("at most %s\n", $quote['maxCharge']); // decide antes de gastar

$task = $client->createTask(
    model: $quote['model'],
    input: ['prompt' => 'a lantern in fog'],
    idempotencyKey: SpicyClient::idempotencyKey(),
    quoteId: $quote['quoteId'],
    expectedCost: $quote['estimatedCost'],
);

$final = $client->waitForTerminal($task['taskId']);
foreach (SpicyClient::readyAssets($final) as $asset) {
    echo $asset['url'], "\n";
}

Cada llamada devuelve el data ya decodificado de la envoltura de la respuesta como un array asociativo normal, así que léelo con $quote['maxCharge'], no con $quote->maxCharge.

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(); cada modelo acepta campos distintos.

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. Compáralos con SpicyClient::compareUsd(). En esta API no hay créditos ni puntos por ninguna parte.

waitForTerminal devuelve el control en cualquier estado terminal, y un estado terminal no es un estado correcto. Comprueba el estado antes de leer la salida, y lee primero SpicyClient::outputText($final) en los endpoints que responden con texto en lugar de con archivos.

Sáltate la primera consulta

waitSeconds mantiene la conexión abierta hasta que la tarea termina, así que un trabajo corto no necesita ningún bucle de sondeo. No promete que la tarea esté terminada. Si se agota ese margen, recibes la respuesta normal de aceptación —misma forma, distinto state—, así que ramifica según el estado, nunca según el hecho de que hayas pasado el parámetro:

$submitted = $client->createTask(
    model: 'MODEL_ID_FROM_CATALOG',
    input: ['prompt' => 'a lantern in fog'],
    idempotencyKey: SpicyClient::idempotencyKey(),
    waitSeconds: 30,
);

if (!SpicyClient::isTerminal($submitted)) {
    $submitted = $client->waitForTerminal($submitted['taskId']);
}

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 en una sola llamada la subida en tres pasos —ticket, PUT al almacenamiento y confirmación— y devuelve un URI spicy://. Ese URI es lo que va en la entrada.

$uploaded = $client->uploadFile('/path/to/reference.png');
$task = $client->createTask(
    model: 'MODEL_ID_FROM_CATALOG',
    input: ['image' => $uploaded['uri'], 'prompt' => 'slow dolly in'],
    idempotencyKey: SpicyClient::idempotencyKey(),
);

Una URL HTTPS accesible públicamente puede ir directamente en la entrada y no necesita ninguna subida. Se aceptan imágenes de hasta 10 MiB, y audio y video de hasta 90 MiB; consulta Carga y descarga de medios.

Webhooks

use SpicyApi\SpicyWebhook;

// El secreto va primero. Los demás argumentos son opcionales: se leen por ti $_SERVER
// y los bytes en bruto de php://input, salvo que un framework ya los haya consumido.
$delivery = SpicyWebhook::verifyRequest(getenv('SPICY_WEBHOOK_SECRET') ?: '');

// Dentro de un framework que almacene el cuerpo en un búfer, pásale tú los bytes exactos:
$delivery = SpicyWebhook::verifyRequest(
    secret: getenv('SPICY_WEBHOOK_SECRET') ?: '',
    server: $_SERVER,
    rawBody: $request->getContent(),
);

Tres formas en las que el código PHP se equivoca aquí, y que esta clase evita todas:

  • Volver a serializar el cuerpo antes de verificarlo. Un json_decode seguido de un json_encode cambia los bytes, y la firma ya no volverá a coincidir. Verifica los bytes en bruto.
  • Comparar firmas con == o ===. Más allá del tiempo que tardan, == sobre dos cadenas que empiezan por 0e… las compara como números: "0e123" == "0e456" da true. Aquí se usa hash_equals.
  • Saltarse la comprobación de la marca de tiempo. Una firma válida sigue siendo válida para siempre; sin una ventana de frescura, una entrega capturada se puede reproducir más tarde.

Las entregas se reintentan, así que deduplica por el ID de entrega. El formato completo de las entregas y el calendario de reintentos están en Webhooks.

Saldo, consumo e historial

$balance = $client->getBalance();                        // disponible / retenido / total, en cadenas USD
$usage = $client->getUsage('2026-09-01', '2026-09-08');  // gasto liquidado en [from, to)

foreach ($client->iterTasks(state: 'succeeded') as $summary) {
    echo $summary['taskId'], "\n";                       // todas las páginas, repitiendo los filtros por ti
}

held es el dinero que retienen las tareas que aún no se han liquidado, así que no está ni gastado ni disponible. Las fechas son días naturales en UTC, YYYY-MM-DD, y forman un intervalo semiabierto [from, to) de 92 días como máximo. Las tres llamadas solo ven la clave de API que las hace: ni las claves hermanas ni las generaciones de la consola se incluyen.

getUsage() sirve para cuadrar cuentas, no para seguir el progreso. Lleva un presupuesto de solicitudes común a toda la cuenta que comparten todas las claves, y ese limitador falla en cerrado: sondearlo puede dejar a la cuenta entera sin acceso a sus propios informes. Para seguir una tarea usa getTask().

listTasks() devuelve una página como {items, hasMore, nextCursor}. Paginar a mano significa repetir todos los filtros sin cambiarlos y mover solo el cursor, y parar cuando hasMore sea falso o nextCursor esté vacío: comprobar solo hasMore convierte un fallo puntual del servidor en un bucle infinito de llamadas de pago que parece un cuelgue. iterTasks() hace todo eso por ti.

Errores

Los fallos lanzan SpicyApiError, que lleva $businessCode, $requestId y $retryAfterSeconds. Ramifica según el código de negocio, nunca según el mensaje: los mensajes se traducen, los códigos no.

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
40004ningún despliegue atiende esa combinación de parámetroscambia el parámetro que nombra el mensaje
40201saldo insuficienteañade fondos
409la clave de idempotencia se reutilizó para una solicitud distintareenvía la solicitud original con esa clave, o la solicitud cambiada con una clave nueva
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 utilizableactualiza el catálogo
50302una generación síncrona falló aguas arriba y se reembolsóreintenta con una clave de idempotencia nueva

$error->guidance() devuelve ese mismo consejo en tiempo de ejecución. Todos los códigos están en Errores.

Dos cosas que te ahorrarán dinero

Usa una sola clave de idempotencia por envío y reutilízala en cada reenvío: que se pierda una respuesta no demuestra que la tarea no se haya creado. Consulta Idempotencia.

Una tarea que sale bien se cobra, aunque el resultado te decepcione. quote() no retiene nada y no crea nada.

Lo que este paquete no hace

  • Modelos de texto. Hablan los formatos de OpenAI, Anthropic y Gemini; los clientes de PHP consolidados para esos formatos ya funcionan contra https://api.spicyapi.ai/v1. Consulta Texto y streaming.
  • Código del lado del navegador. La clave vive en tu servidor, nunca en nada que envíes a quien visita tu sitio.
  • 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 paquete tiene licencia MIT y se desarrolla en abierto en github.com/spicyapi-ai/spicy-php. Su README es el recorrido completo más corto por el cliente, y examples/ contiene scripts listos para ejecutar; esta página añade los conceptos que lo rodean.

Siguientes pasos

En esta página