SDKs
Official clients for TypeScript and Python. Both have retries with backoff, idempotency keys, streaming, typed errors and webhook verification built in. Source: github.com/tovanoai/sdk.
| TypeScript | Python | |
|---|---|---|
| Install | npm install tovano | pip install tovano |
| Runtimes | Node 20+, Bun, Deno, Workers, browsers | Python 3.9 to 3.14, sync and async |
| Dependencies | None | httpx only |
The npm and PyPI packages are being published. Until then, install from github.com/tovanoai/sdk.
Both read TOVANO_API_KEY and use https://api.tovano.ai by default. Money is always an integer number of micro-dollars in fields ending in _micro.
TypeScript
npm install tovano
import { Tovano, formatUsd } from "tovano";
const tovano = new Tovano(); // reads TOVANO_API_KEY
// or: new Tovano({ apiKey: "tvn_live_...", baseUrl: "https://api.tovano.ai" })
const task = await tovano.tasks.run({
model: "image/demo-1",
input: { prompt: "Product photo of a blue ceramic mug", n: 2 },
customer: "client-0482",
});
console.log(task.status, task.units, formatUsd(task.charged_micro)); // completed 2 $0.04
Tasks
const task = await tovano.tasks.create({
model: "video/demo-1",
input: { prompt: "Waves on a beach", duration: 5 },
webhook_url: "https://example.com/tovano-webhook",
});
const done = await tovano.tasks.wait(task.id, { timeoutMs: 120_000 });
if (done.status === "failed") console.log(done.error?.message, "(not charged)");
Chat, with streaming
for await (const chunk of tovano.chat.completions.create({
model: "text/demo-chat-1",
messages: [{ role: "user", content: "Three captions for a mug ad" }],
stream: true,
})) {
process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}
Balance, usage and webhooks
const balance = await tovano.balance(); // available_micro, reserved_micro, spent_this_month_micro
const usage = await tovano.usage({ group_by: "customer", from: "2026-11-01" });
import { webhooks } from "tovano";
const event = await webhooks.constructEvent(rawBody, request.headers.get("tovano-signature"), process.env.TOVANO_WEBHOOK_SECRET!);
Python
pip install tovano
from tovano import Tovano, format_usd
client = Tovano() # reads TOVANO_API_KEY
# or: Tovano(api_key="tvn_live_...", base_url="https://api.tovano.ai")
task = client.tasks.run(
model="image/demo-1",
input={"prompt": "Product photo of a blue ceramic mug", "n": 2},
customer="client-0482",
)
print(task.status, task.units, format_usd(task.charged_micro)) # completed 2 $0.04
Async
import asyncio
from tovano import AsyncTovano
async def main():
async with AsyncTovano() as client:
image, voice = await asyncio.gather(
client.tasks.run(model="image/demo-1", input={"prompt": "Mug"}),
client.tasks.run(model="audio/demo-tts-1", input={"text": "Hello"}),
)
asyncio.run(main())
Chat, with streaming
with client.chat.completions.create(
model="text/demo-chat-1",
messages=[{"role": "user", "content": "Three captions for a mug ad"}],
stream=True,
) as stream:
for chunk in stream:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="")
Errors
Every failed call raises or throws a typed error with status, code and message, the same as the error format.
| Error | Status | Code |
|---|---|---|
AuthenticationError | 401 | invalid_api_key |
InsufficientCreditsError | 402 | insufficient_credits |
SpendLimitReachedError | 402 | spend_limit_reached |
NotFoundError | 404 | not_found |
InvalidRequestError | 422 | invalid_request |
RateLimitError | 429 | rate_limited (retried for you) |
ProviderError | 502 | provider_error (not charged) |
Connection errors, timeouts, 408, 429 and 5xx are retried with backoff, with the same Idempotency-Key on every attempt.
OpenAI clients
Any OpenAI client works for text models: set its base URL to https://api.tovano.ai/v1 and use your Tovano key. See Chat.