The varg API is a single REST API for AI media generation across 8 providers (Fal, ElevenLabs, Higgsfield, PiAPI, HeyGen, Together, Groq, Rendi). One API key, ~80 models, async jobs, stable output URLs at s3.varg.ai, and transparent credit billing.
Base URL
Authentication
All requests (except GET /pricing) require a varg API key:
Authorization: Bearer varg_xxx
Get your API key at app.varg.ai or run bunx vargai login. See Authentication for details.
Core concepts
The API vocabulary has three levels:
| Concept | Example | What it is |
|---|
| Tool | image, video, speech | A capability. Each tool has its own endpoint (POST /v2/image) and unified input schema. |
| Model | flux_schnell, kling_v3 | What you put in the model field. One model can be served by multiple providers. |
| Provider | fal, elevenlabs | Who actually runs the generation. varg picks the best provider automatically. |
Tools: image, video, speech, music, transcription, ffmpeg, render, pipeline. Discover them at runtime via GET /v2/tools.
Quick start
Every generation is an async job: create it, poll until terminal, read the output URL.
# 1. Create a job (202 Accepted)
JOB=$(curl -s -X POST https://api.varg.ai/v2/video \
-H "Authorization: Bearer $VARG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "kling_v3", "prompt": "cat jumping over a fence", "duration": 5}')
JOB_ID=$(echo $JOB | jq -r '.id')
# 2. Poll until status is terminal
curl -s https://api.varg.ai/v2/jobs/$JOB_ID \
-H "Authorization: Bearer $VARG_API_KEY" | jq '{status, output}'
# 3. When completed, the video URL is in output.outputs[0].url
# {
# "status": "completed",
# "output": {
# "version": "v1",
# "outputs": [{
# "url": "https://s3.varg.ai/files/acc_x/file_abc.mp4",
# "file_id": "file_abc123",
# "media_type": "video/mp4"
# }]
# }
# }
Creation responses include ready-made lifecycle URLs:
{
"id": "job_a1b2c3d4e5f6",
"status": "queued",
"estimated_cost_cents": 221,
"urls": {
"self": "https://api.varg.ai/v2/jobs/job_a1b2c3d4e5f6",
"status": "https://api.varg.ai/v2/jobs/job_a1b2c3d4e5f6/status",
"cancel": "https://api.varg.ai/v2/jobs/job_a1b2c3d4e5f6/cancel",
"retry": "https://api.varg.ai/v2/jobs/job_a1b2c3d4e5f6/retry",
"refresh": "https://api.varg.ai/v2/jobs/job_a1b2c3d4e5f6/refresh"
}
}
Job lifecycle
queued → submitting → running → completed | failed | cancelled
| Status | Meaning |
|---|
queued | Accepted, waiting for a provider slot |
submitting | Being sent to the provider |
running | The provider is generating |
completed | Done — read output.outputs[0].url |
failed | Check the error field; retry with POST /jobs/{id}/retry |
cancelled | Aborted via POST /jobs/{id}/cancel |
Poll GET /v2/jobs/{id} (full view) or GET /v2/jobs/{id}/status (lightweight, includes progress 0..1 and progress_message). A stuck job is guaranteed to reach a terminal status — the platform times out jobs after 30 minutes.
Instead of polling, set options.webhook_url in your request. When the job finishes, varg POSTs the job snapshot to your URL, signed with X-Varg-Signature: v1=<hmac-sha256>, with 8 retries and exponential backoff.
All generation endpoints take a flat JSON body: model plus tool-specific fields at the top level.
{
"model": "kling_v3",
"prompt": "cat jumping over a fence",
"duration": 5,
"aspect_ratio": "16:9",
"files": [{ "url": "https://s3.varg.ai/files/acc_x/cat.jpg" }],
"provider_options": { "fal": { "seed": 42 } },
"options": { "webhook_url": "https://example.com/hook" }
}
| Field | Applies to | Description |
|---|
model | all | Model id — required |
prompt | image, video, music | Text prompt (max 2000 chars) |
text | speech | Text to speak (max 10000 chars) |
voice | speech | Voice name |
audio_url | transcription | Public URL of the audio file |
duration | video, music | Seconds |
aspect_ratio | image, video | e.g. "16:9", "9:16" |
files | image, video | Input files by URL (max 10) — start frames, edit sources, lipsync inputs |
provider_options | all | Provider-native overrides, keyed by provider |
options.webhook_url | all | Completion webhook |
Media inputs are always passed by URL — upload local files first via POST /v2/files to get a stable varg URL.
Model resolution
Most requests should just use the model name. Pass a canonical varg name (seedance_2, kling_v3, flux_schnell) and varg auto-routes by priority and input shape — if your request includes files, routes that map files are preferred; the first route whose schema accepts your input wins.
{ "model": "seedance_2" } // canonical name — varg picks the route (recommended)
For advanced control, two more address forms exist:
{ "model": "fal:seedance_2" } // provider prefix — routes filtered to fal
{ "model": "fal:bytedance/seedance-2.0/fast/reference-to-video" } // exact model_key — pins one route
- Provider prefix — same auto-routing, restricted to routes on that provider.
- Exact
model_key — pins one specific route. model_key values are a permanent public contract and safe to hardcode.
Every route’s model_key is listed in GET /v2/models.
Two guarantees:
- No silent substitution. varg only picks which route serves the model you named — never a different model family.
- Transparent routing. The job records which provider model actually ran (
provider, provider_model fields), and POST /v2/estimate returns the resolved_model_key before you submit.
Within a model, input shape can select a variant automatically: sending an image in files to kling_v3 routes to its image-to-video endpoint. Same family, same pricing rules.
Model ids use underscores (kling_v3, flux_schnell). Dashed spellings (kling-v3) are accepted and normalized automatically. Browse all models and prices at GET /v2/pricing (public, no auth).
Model catalog
GET /v2/models (public, no auth) returns the full catalog: every model with its routes, pricing, and operational hints. Filter by tool with ?tool=video.
curl -s "https://api.varg.ai/v2/models?tool=video"
{
"version": "v1",
"data": [
{
"model": "seedance_2",
"tool": "video",
"routes": [
{
"model_key": "fal:bytedance/seedance-2.0/text-to-video",
"provider": "fal",
"modality": ["text-to-video"],
"output_type": "video",
"description": "Seedance 2.0 standard tier, up to 4k",
"priority": 1,
"accepts_files": false,
"pricing": {
"pricing_id": "ppr_...",
"rule": { "type": "per_second_conditional", "param": "duration", "rates": ["..."] },
"user_price_estimate_cents": 479
},
"ops": {
"delivery": "webhook",
"poll_interval_ms": 5000,
"estimated_duration_ms": 180000,
"max_timeout_ms": 1800000
},
"schema_url": "/v2/tools/video?model=fal:bytedance/seedance-2.0/text-to-video"
}
]
}
]
}
Each route’s model_key can be used directly in the model field to pin that route.
Discovering schemas at runtime
Agents and dynamic clients can discover the full API without reading docs:
# List all tools
curl -s https://api.varg.ai/v2/tools -H "Authorization: Bearer $VARG_API_KEY"
# Get the image tool's input schema, output shape, and a worked example
curl -s https://api.varg.ai/v2/tools/image -H "Authorization: Bearer $VARG_API_KEY"
# Get the explicit per-provider options for a specific model
curl -s "https://api.varg.ai/v2/tools/image?model=flux_schnell" \
-H "Authorization: Bearer $VARG_API_KEY"
# Call any tool generically (same as POST /v2/image)
curl -s -X POST https://api.varg.ai/v2/tools/image/call \
-H "Authorization: Bearer $VARG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "flux_schnell", "prompt": "a cat astronaut"}'
Pricing and billing
1 credit = 1 cent = $0.01. The billing flow:
- Reserve — at job creation, the estimated cost is held from your balance (402 if insufficient).
- Commit — on completion, the hold is charged and a usage record written.
- Release — on failure, the hold is returned (most models; some bill partial work).
# Price a request WITHOUT creating a job
curl -s -X POST https://api.varg.ai/v2/estimate \
-H "Authorization: Bearer $VARG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "seedance_2", "prompt": "cat", "duration": 5}'
# {"valid": true, "model": "seedance_2", "tool": "video",
# "resolved_model_key": "fal:bytedance/seedance-2.0/text-to-video",
# "pricing": {"provider_cost_cents": 152, "user_price_cents": 160,
# "markup_percent": 5, "pricing_id": "ppr_..."}}
# Check your balance
curl -s https://api.varg.ai/v2/billing/balance \
-H "Authorization: Bearer $VARG_API_KEY"
# {"available": 8200, "reserved": 300, "total_balance": 8500, ...}
Each job carries its pricing in the response: pricing.estimated, pricing.actual, pricing.billed_units (e.g. {"seconds": 6}), and a pricing_id that pins the price rule the job was charged under — mid-flight price changes never affect running jobs.
Caching
Results are cached by canonical input. Repeating an identical request returns a completed job instantly with actual_cost_cents: 0 and pricing.cached: true — cache hits are free.
Idempotency
Pass an Idempotency-Key header on any job-creating POST to make retries safe:
curl -s -X POST https://api.varg.ai/v2/image \
-H "Authorization: Bearer $VARG_API_KEY" \
-H "Idempotency-Key: my-unique-key-1" \
-H "Content-Type: application/json" \
-d '{"model": "flux_schnell", "prompt": "a cat astronaut"}'
The first request creates the job (202). Retrying with the same key returns the same job (200) — no duplicate charge, no duplicate generation.
Files and lineage
Outputs are mirrored to varg storage and served from stable s3.varg.ai URLs. Every output gets a file record with an AI-generated title, a thumbnail, and lineage back to the job that created it:
# Upload an input file (raw binary body, max 200MB)
curl -s -X POST https://api.varg.ai/v2/files \
-H "Authorization: Bearer $VARG_API_KEY" \
-H "Content-Type: image/jpeg" \
-H "X-Filename: cat.jpg" \
--data-binary @cat.jpg
# {"file_id": "file_abc123", "url": "https://s3.varg.ai/files/...", ...}
# Which job made this file? (get the recipe: prompt, model, cost)
curl -s -X POST https://api.varg.ai/v2/lineage \
-H "Authorization: Bearer $VARG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"file_id": "file_abc123"}'
Send X-Content-Hash: sha256:<hex> on upload for content deduplication, or pre-check with GET /v2/files/check?hash=.
All errors use one envelope:
{
"error": {
"code": "model_not_found",
"message": "Unknown model: flux-shnell",
"details": null
}
}
| Status | Code | Description |
|---|
| 400 | invalid_request, invalid_json | Malformed body |
| 401 | unauthorized | Missing or invalid API key |
| 402 | insufficient_balance | Balance too low to reserve the estimated cost |
| 404 | model_not_found, tool_not_found, job_not_found, file_not_found | Unknown resource |
| 409 | — | Job is already terminal (cancel/refresh) |
| 413 | file_too_large | Upload over 200 MB |
| 422 | invalid_request | Body failed schema validation (details included) |
| 429 | rate_limited | Too many requests — honor Retry-After |
| 503 | no_pricing | Model temporarily has no active pricing |
model_not_found errors include suggestions in details: did_you_mean lists close model names, and models_url points to /v2/models for the full catalog.
Rate limits
Rate limiting is a sliding window per API key. Every job-creation response carries:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 2026-07-01T10:01:00.000Z
On 429, wait for Retry-After seconds and retry (ideally with the same Idempotency-Key).
Migrating from v1
The v1 gateway (api.varg.ai/v1) is frozen. Key differences in v2:
| v1 | v2 |
|---|
job_id field | id field |
output.url (single) | output.outputs[] (array, each with file_id) |
GET /jobs/{id}/stream (SSE) | Removed — poll or use options.webhook_url |
DELETE /jobs/{id} | POST /jobs/{id}/cancel |
GET /balance | GET /billing/balance (richer breakdown) |
GET /usage | GET /billing/usage |
POST /ffmpeg/trim etc. | Single POST /ffmpeg, operation selected by model |
POST /ffmpeg/probe | POST /files/probe |
GET /voices | Not yet ported — pass voice by name |
| Model names with dashes | Underscores canonical (dashes still accepted) |
| Flat error shape | {"error": {"code", "message"}} envelope |