One API for AI Models: Image, Video, Music and Chat
How one API key and one prepaid balance cover chat, image, video and music models. Endpoints, working code, cost checks and failure handling.
A unified AI API gives you one key, one balance and one request format for many models: chat models answer through an OpenAI-compatible endpoint, while image, video and music models run as async tasks that you submit and then collect. With Jantaris that means POST /v1/chat/completions for text and POST /v1/generate plus GET /v1/tasks/{id} for everything else. You switch models by changing one string, not by signing up for another vendor.
I built Jantaris because my own projects kept collecting API keys. A chat model from one company, an image model from another, a video model from a third, each with its own billing page, its own minimum top-up and its own idea of what an error looks like. This post shows how the single-API approach works in practice, with code you can run today.
Why use one API for multiple AI models
The honest reason is less plumbing. Each vendor SDK is fine on its own. The trouble starts when a project needs three of them: three auth schemes, three retry policies, three places where the card can expire.
With one API for AI models you get:
- One key (
sk-jt-...) that works for Claude Sonnet 5, GPT Image 2.5, Seedance 2.5 and Mureka V9 alike. - One prepaid balance. No subscription, no minimum commitment, and the balance does not expire.
- One price format. Every model has a price per unit: per image, per second of video, per 1M tokens. You see it on the pricing page before you call anything.
- One error format. Every error is
{ "error": { "type": "...", "message": "..." } }with a predictable HTTP code.
There is a practical bonus too. When a model is available from two infrastructure sources, Jantaris can fail over automatically for part of the catalog. Your code does not change.
How a unified AI API works: two request styles
AI models fall into two groups by how long they take.
| Model type | Typical time | Request style | Endpoint |
|---|---|---|---|
| Chat and code | seconds, streamed | synchronous | POST /v1/chat/completions or POST /v1/messages |
| Image | a few seconds to a minute | async task, or sync with ?wait= | POST /v1/generate |
| Video | one to several minutes | async task + polling or webhook | POST /v1/generate |
| Music and audio | tens of seconds to minutes | async task | POST /v1/generate |
Chat stays synchronous because the OpenAI and Anthropic formats are what every tool already speaks. Media generation is async because a 10-second video clip can take minutes, and holding an HTTP connection open that long breaks on proxies, serverless functions and mobile networks.
The full endpoint list
| Method | Path | What it does |
|---|---|---|
| POST | /v1/chat/completions | Chat, OpenAI-compatible, stream: true supported |
| POST | /v1/messages | Chat, Anthropic Messages-compatible (Claude Code uses this) |
| POST | /v1/generate | Submit an image, video or audio job, returns a task (202) |
| GET | /v1/tasks/{id} | Task status and output |
| GET | /v1/models | Public model catalog with prices |
| GET | /v1/models/{id} | One model's details and parameter schema |
| GET | /v1/balance | Your current balance |
The base URL is https://jantaris.com/v1 and the key goes in Authorization: Bearer sk-jt-... (or x-api-key). Details are in authentication.
Chat models through the OpenAI format
If you have ever used the OpenAI SDK, you already know this part. Change the base URL, keep the rest.
from openai import OpenAI
client = OpenAI(
base_url="https://jantaris.com/v1",
api_key="sk-jt-...",
)
resp = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "Write a product name for a ceramic kettle."}],
)
print(resp.choices[0].message.content)Swap claude-sonnet-5 for gpt-6-sol, gemini-3.8-flash or deepseek-v4.1-flash and the same code runs. I cover this pattern in depth, including streaming and model fallback, in Call Claude, Gemini, DeepSeek and GPT with the OpenAI SDK.
Image, video and music through one generate endpoint
Every media model uses the same body shape: a model id and an input object. The inputs share names across models where the idea is the same.
- Image:
prompt,negative_prompt,aspect_ratio(1:1,16:9,9:16,4:3,3:4),num_images(1 to 4),image_urlsfor edits and references,seed - Video:
prompt,durationin seconds,resolution(480p,720por1080pdepending on the model),aspect_ratio,image_urlfor the first frame,generate_audio - Music and audio:
prompt,duration,lyrics,instrumental
Model-specific options live in the schema at GET /v1/models/{id}. Check it before you rely on a parameter.
Submitting a video task
curl -X POST https://jantaris.com/v1/generate \
-H "Authorization: Bearer sk-jt-..." \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.5",
"input": {
"prompt": "A lighthouse at dawn, slow aerial push-in, waves breaking on rocks",
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9"
}
}'The response comes back immediately with status 202:
{
"id": "task_8f2k...",
"object": "task",
"status": "queued",
"model": "seedance-2.5",
"estimated_cost": { "amount": "X.XX", "currency": "USD" },
"poll_url": "https://jantaris.com/v1/tasks/task_8f2k..."
}Note estimated_cost. The price is calculated before the job is sent to the model, so you know what a request costs before it runs. That amount is held on your balance while the task works.
Collecting the result
import time
import requests
API = "https://jantaris.com/v1"
HEADERS = {"Authorization": "Bearer sk-jt-..."}
def wait_for(task_id, every=5, timeout=900):
deadline = time.time() + timeout
while time.time() < deadline:
task = requests.get(f"{API}/tasks/{task_id}", headers=HEADERS, timeout=30).json()
if task["status"] in ("succeeded", "failed"):
return task
time.sleep(every)
raise TimeoutError(task_id)
task = wait_for("task_8f2k...")
if task["status"] == "succeeded":
for item in task["output"]:
print(item["type"], item["url"])
else:
print("failed:", task["error"])A finished task looks like this:
{
"id": "task_...",
"status": "succeeded",
"model": "seedance-2.5",
"output": [{ "type": "video", "url": "https://...", "duration": 5, "width": 1280, "height": 720 }],
"cost": { "amount": "X.XX", "currency": "USD" },
"error": null
}For production pipelines, skip polling and pass a webhook_url. I walk through signature checks and retries in Building an AI video pipeline with async tasks and webhooks.
Images without polling: the wait parameter
Images usually finish in seconds, so polling is overkill. Add ?wait=60 (max 120) and the server holds the request until the task is done, then returns the finished task in one response.
r = requests.post(
f"{API}/generate?wait=60",
headers=HEADERS,
json={
"model": "nano-banana-pro",
"input": {"prompt": "Minimal poster, the word NORTH in bold serif", "aspect_ratio": "3:4"},
},
timeout=130,
)
task = r.json()
print(task["status"], task.get("output"))Always check status in the response. If it is not succeeded yet, fall back to polling the task by its id.
Music in the same format
const res = await fetch("https://jantaris.com/v1/generate", {
method: "POST",
headers: {
"Authorization": "Bearer sk-jt-...",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "mureka-v9",
input: {
prompt: "Warm lo-fi hip hop, soft piano, vinyl crackle, 80 bpm",
lyrics: "[Verse]
City lights are fading slow
[Chorus]
Stay a little longer",
},
}),
});
const task = await res.json();
console.log(task.id, task.estimated_cost);Same key, same task object, same polling code as the video above.
What happens when a generation fails
This is the part that matters most once you run real volume. Video models fail sometimes: a content filter trips, a provider times out, a job gets stuck.
On Jantaris a failed generation costs nothing. When a task ends as failed, the hold on your balance is released in full and cost is 0. You pay for results, not attempts.
Errors on submission come back as JSON with a clear code:
| Code | Meaning | What I do in code |
|---|---|---|
| 401 | Bad or missing key | Stop, fix config |
| 402 | Balance too low | Alert, top up, retry later |
| 404 | Model id does not exist | Check /v1/models |
| 422 | Invalid parameters | Log error.message, fix input |
| 429 | Rate limit | Back off and retry |
| 502 | Upstream provider error | Retry with backoff |
The full list is in errors.
Checking prices and balance from code
The model catalog is public, so you can read prices without a key:
curl https://jantaris.com/v1/models
curl https://jantaris.com/v1/models/seedance-2.5And your balance:
curl https://jantaris.com/v1/balance -H "Authorization: Bearer sk-jt-..."A few current rates for reference: Seedance 2.5 runs at from $0.261 per second, GPT Image 2.5 at $0.0432 per image, and Claude Sonnet 5 at $3.24 / $16.20 per 1M tokens. The live list is on the pricing page. Top-ups start at $10, with ready-made packs of $10, $25, $50, $100 and $250.
Things to know before you build on it
- Download your outputs. Result links are valid for a limited time. Copy files to your own storage as soon as a task succeeds. The Studio keeps a generation history on your account, the API assumes you store files yourself.
- Your data stays yours. Prompts and outputs are not used to train models.
- Use the schema. Not every video model supports every resolution or duration.
GET /v1/models/{id}tells you what the model accepts, and a wrong value returns 422 before anything is charged. - Not a coder? The same models are in the browser Studio, on the same balance.
If you want to start now, the quickstart takes about five minutes from sign-up to first video.
FAQ
What is a unified AI API?
It is a single API that routes requests to many AI models from different vendors. You use one key, one billing balance and one request format, and you choose the model with a model parameter instead of integrating each vendor separately.
Can I use the OpenAI SDK with Jantaris?
Yes. Set base_url="https://jantaris.com/v1" and your sk-jt-... key. The /v1/chat/completions endpoint is OpenAI-compatible, including streaming, and /v1/messages is Anthropic-compatible for tools like Claude Code.
Do I pay for failed image or video generations?
No. If a task ends with status failed, its cost is 0 and the amount held on your balance is released in full.
How do I know what a request will cost?
Every model has a public per-unit price in /v1/models and on the pricing page. When you submit a generation, the 202 response includes estimated_cost, calculated before the job runs.
Does my balance expire?
No. The balance is prepaid, has no expiry date, and there is no subscription or minimum commitment.












