For AI agents: a documentation index is available at /llms.txt. Markdown versions of all documentation pages are available by appending .md to the URL path.
Visa Keys let an approved account run paid Visa tools from a backend, scheduled job, or headless agent without opening an MCP approval prompt for every call. A key spends from the owner's prepaid balance, and server-side caps, allowlists, environment scoping, and idempotency protect the money path.
There are three ways to spend with a key, all sharing the same auth, prepaid balance, caps, rate limits, and idempotency:
POST /v1/api/tools/:tool/execute runs any catalog tool over plain HTTP.POST /v1/chat/completions is a drop-in model provider for any OpenAI-compatible client./mcp so an MCP client can discover and run Visa tools with no local CLI.Use the CLI for the common key lifecycle, then send the key to the HTTP API:
visa-cli keys create my-demo-app --tools fal-flux-pro,or-gpt-4o-mini --daily-cap 5 --total-cap 200
visa-cli keys list
visa-cli keys revoke 1
The raw VisaKey_... secret is printed once when the key is created. Store it in your app secret manager. Do not commit it, paste it into chat, or expose it to browsers.
Visa Keys are immutable after creation. To change a label, tool scope, cap, CIDR allowlist, expiry, or secret, create a replacement key with the new policy, deploy it to the consuming service, then revoke the old key.
A machine-readable OpenAPI 3.1 spec describes the key-management, direct-execution, and chat route groups — including every request field, response shape, and error_code. Point your SDK generator or API client at it.
Two distinct credentials are used in this API — do not confuse them.
| Credential | Used for | How to send |
|---|---|---|
Visa Key (VisaKey_...) |
Executing paid tools, chat completions, and remote MCP | X-Api-Key: VisaKey_... (chat also accepts Authorization: Bearer VisaKey_...) |
| Session token (owner login) | Key lifecycle — create, list, retrieve, revoke keys | Authorization: Bearer $VISA_SESSION_TOKEN |
The execution routes authenticate with the Visa Key itself (X-Api-Key). The key-management routes authenticate with an approved user's session token — the same identity used in the dashboard and MCP — not with a Visa Key. The CLI (visa-cli keys ...) handles the session token for you.
On /v1/chat/completions the Visa Key is accepted as either Authorization: Bearer VisaKey_... (the OpenAI SDK default) or X-Api-Key: VisaKey_...; both map to the same key. Bearer is bridged to X-Api-Key only on that endpoint — direct execution still requires X-Api-Key.
| Environment | Base URL |
|---|---|
| Production | https://auth.visacli.sh |
| Staging or preview | https://auth-visa-code-preview.up.railway.app |
Visa Keys are environment-scoped, and each environment has its own key store. A key bound to one environment but sent to the other is rejected — 401 KEY_ENVIRONMENT_MISMATCH when the key is recognized but bound elsewhere, or 401 AUTH_INVALID when it simply is not in that environment's store.
/v1 suffix (base_url="https://auth.visacli.sh/v1") — the SDK appends /chat/completions itself. Raw curl uses the full path https://auth.visacli.sh/v1/chat/completions.The raw VisaKey_... secret is printed once when the key is created. Store it in your secret manager — never commit it, paste it into chat, or expose it to browsers.
| Prefix | Mode | Notes |
|---|---|---|
VisaKey_... |
Live | Standard live key. Spends real balance. |
visakey_... |
Live | Lowercase live variant (also accepted). |
vk_test_... |
Test / sandbox | Sandbox-only. Rejected on execution and chat with 403 TEST_KEY_NOT_SUPPORTED. |
active or revoked.Create a key with the CLI, then execute a paid tool over HTTP.
Create a scoped key:
visa-cli keys create my-demo-app --tools or-gpt-4o-mini --daily-cap 5 --total-cap 200
Execute a tool with the key:
idem=$(uuidgen | tr '[:upper:]' '[:lower:]')
curl -sS https://auth.visacli.sh/v1/api/tools/or-gpt-4o-mini/execute \
-H "X-Api-Key: $VISA_KEY" \
-H "Idempotency-Key: $idem" \
-H "Content-Type: application/json" \
-d '{"input":{"messages":[{"role":"user","content":"Say hello in one sentence."}]}}'
X-Api-Key and an Idempotency-Key (a UUID v4). Reuse the same idempotency key when retrying the same logical operation.Mint a new scoped Visa Key. Returns the raw secret exactly once. Approved accounts only. Authenticates with a session token.
CLI:
visa-cli keys create my-demo-app --tools or-gpt-4o-mini --daily-cap 5 --total-cap 200
HTTP — POST /v1/api/keys:
curl -sS https://auth.visacli.sh/v1/api/keys \
-H "Authorization: Bearer $VISA_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"label": "my-demo-app",
"allowed_tools": ["or-gpt-4o-mini"],
"daily_cap_cents": 500,
"total_cap_cents": 20000,
"tool_scope": "restricted"
}'
Request body:
| Field | Required | Notes |
|---|---|---|
label |
yes | Human-readable key name. |
allowed_tools |
no | Array of tool ids. Provide at least one with tool_scope: "restricted", or omit for all supported tools. |
tool_scope |
no | restricted or all_supported_tools. Restricted keys need at least one allowed tool. |
daily_cap_cents |
no | Daily cap in cents (clamped to the supported range, default 500). |
total_cap_cents |
no | Cumulative lifetime cap in cents, or null for none. |
allowed_cidrs |
no | CIDR allowlist for source IPs. Malformed CIDRs fail closed (400). |
expires_at |
no | ISO-8601 timestamp with timezone. Must be in the future. |
target_login |
admin only | Mint a key for another approved user. |
mode |
no | live (default) or test. |
Create returns the raw key once (201 Created):
{
"success": true,
"key": "VisaKey_...",
"key_prefix": "VisaKey_abc123...",
"id": 123,
"label": "my-demo-app",
"owner": "octocat",
"allowed_tools": ["or-gpt-4o-mini"],
"tool_scope": "restricted",
"daily_cap_cents": 500,
"total_cap_cents": 20000,
"environment": "production"
}
key field is the full secret and is shown only in this response. It cannot be retrieved again — store it before you move on.List the caller's keys with cursor pagination. Secrets are never returned — only the prefix and metadata. Authenticates with a session token.
CLI:
visa-cli keys list
HTTP — GET /v1/api/keys:
curl -sS "https://auth.visacli.sh/v1/api/keys?limit=25" \
-H "Authorization: Bearer $VISA_SESSION_TOKEN"
Query parameters:
| Param | Notes |
|---|---|
limit |
1–100, default 25. |
starting_after |
Cursor (key id) — move to older keys via next_cursor. |
ending_before |
Cursor — move back toward newer keys via previous_cursor. Do not send both cursors at once. |
List responses include cursor metadata:
{
"success": true,
"keys": [
{
"id": 123,
"key_prefix": "VisaKey_abc123...",
"label": "my-demo-app",
"owner": "octocat",
"allowed_tools": ["or-gpt-4o-mini"],
"tool_scope": "restricted",
"daily_cap_cents": 500,
"total_cap_cents": null,
"environment": "production",
"status": "active",
"last_used_at": "2026-06-22T10:00:00Z",
"created_at": "2026-06-22T09:00:00Z"
}
],
"limit": 25,
"has_more": false,
"next_cursor": null,
"previous_cursor": null
}
Fetch metadata for a single key by its numeric id. The secret is never returned. Authenticates with a session token.
HTTP — GET /v1/api/keys/:id:
curl -sS https://auth.visacli.sh/v1/api/keys/123 \
-H "Authorization: Bearer $VISA_SESSION_TOKEN"
{
"success": true,
"key": {
"id": 123,
"key_prefix": "VisaKey_abc123...",
"label": "my-demo-app",
"owner": "octocat",
"status": "active",
"created_at": "2026-06-22T09:00:00Z"
}
}
An unknown id returns 404 KEY_NOT_FOUND; a malformed id returns 400 INVALID_REQUEST.
Revoke a key. Revoked keys can no longer execute tools. Revoke is idempotent — revoking an already-revoked owned key returns 200 with already_revoked: true. Authenticates with a session token.
CLI:
visa-cli keys revoke 123
HTTP — DELETE /v1/api/keys/:id:
curl -sS -X DELETE https://auth.visacli.sh/v1/api/keys/123 \
-H "Authorization: Bearer $VISA_SESSION_TOKEN"
{ "success": true, "revoked": 123, "already_revoked": false }
A non-admin revoking another user's key returns 403 NOT_KEY_OWNER.
Visa Keys are immutable. Keys are never edited in place — use a create-then-revoke flow:
VisaKey_... secret in the consuming service.This keeps the spend policy and credential lifecycle auditable: every active key has one creation-time policy, one raw secret shown once, and a clear revoke event.
The legacy edit (PATCH /v1/api/keys/:id) and rotate (POST /v1/api/keys/:id/rotate) routes are retired — they always return 410 VISA_KEY_IMMUTABLE.
Run a paid tool directly and charge the key owner's balance. Requires both X-Api-Key and Idempotency-Key. Authenticates with a Visa Key.
Route:
POST /v1/api/tools/:tool/execute
Headers:
| Header | Required | Notes |
|---|---|---|
X-Api-Key |
yes | The VisaKey_... secret. |
Idempotency-Key |
yes | UUID v4 per logical paid operation. Reuse on retries. TTL 24h. |
Content-Type |
yes | application/json |
Request body:
| Field | Required | Notes |
|---|---|---|
:tool (path) |
yes | Tool id or alias. Unknown tools return 404 TOOL_NOT_FOUND. |
input |
yes | Tool-specific parameters object. Send {} for tools with no parameters. |
max_cents, dry_run, stream: true, session budget ids, voucher fields, or raw top-level tool parameters. Put all provider parameters under input — the top-level object must contain only the input key (any extra key returns 400).Example:
idem=$(uuidgen | tr '[:upper:]' '[:lower:]')
curl -sS https://auth.visacli.sh/v1/api/tools/or-gpt-4o-mini/execute \
-H "X-Api-Key: $VISA_KEY" \
-H "Idempotency-Key: $idem" \
-H "Content-Type: application/json" \
-d '{"input":{"messages":[{"role":"user","content":"Say hello in one sentence."}]}}'
Success responses use a stable tool-execution envelope:
{
"success": true,
"object": "tool_execution",
"tool": "or-gpt-4o-mini",
"result": {},
"usage": {
"charged_cents": 0,
"charged_micros": "4"
},
"receipt": { "version": 2, "actual_micros": "4", "charged_micros": "4" },
"receipt_v2": { "version": 2, "actual_micros": "4", "charged_micros": "4" },
"idempotent_replay": false
}
result carries the tool's full provider output (an OpenAI-style chat.completion, image URLs, and so on). Both receipt and receipt_v2 are returned and populated. charged_cents floors to whole cents — it is 0 for sub-cent calls, so use charged_micros (USD x 1,000,000) for the exact amount. Rate-limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, RateLimit-Policy) accompany every response.
POST /v1/chat/completions is a drop-in OpenAI Chat Completions endpoint backed by a Visa Key. Point any OpenAI-compatible client — the OpenAI SDK, Hermes, OpenClaw, LiteLLM, the Vercel AI SDK — at the Visa Key base URL with your key as the API key, and it works as a paid model provider. The model sees your tools and returns tool_calls; your agent runtime executes them as usual.
It reuses the same auth, spend caps, idempotency, and settlement as direct execution. Visa billing metadata comes back in X-Visa-Key-* response headers, never in the OpenAI JSON body.
Auth. Send the Visa Key either as Authorization: Bearer VisaKey_... (the OpenAI SDK default) or as X-Api-Key: VisaKey_.... Both work on this endpoint.
Request fields. Standard OpenAI Chat Completions body. model and messages are required; tools, tool_choice, temperature, top_p, max_tokens, stop, response_format, seed, and penalties are forwarded. model accepts a Visa catalog id (or-gpt-4o-mini, or-claude-sonnet, ...) or a common alias (gpt-4o-mini, claude-3-5-sonnet, ...) — see Supported Models. An unknown, non-chat, or not-allowed model returns 404 model_not_found.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://auth.visacli.sh/v1",
api_key=os.environ["VISA_KEY"], # your VisaKey_... secret
)
resp = client.chat.completions.create(
model="gpt-4o-mini", # or a Visa catalog id like or-gpt-4o-mini
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}},
},
}],
)
print(resp.choices[0].message.tool_calls)
import OpenAI from 'openai'
const client = new OpenAI({
baseURL: 'https://auth.visacli.sh/v1',
apiKey: process.env.VISA_KEY, // VisaKey_...
})
const resp = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Hello!' }],
})
curl -sS https://auth.visacli.sh/v1/chat/completions \
-H "Authorization: Bearer $VISA_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello!"}]
}'
Or send the key as -H "X-Api-Key: $VISA_KEY" — both work on this endpoint.
Hermes works with any OpenAI-compatible endpoint. Configure a custom provider in config.yaml:
provider:
type: openai-compatible
base_url: https://auth.visacli.sh/v1
api_key: <your-visa-key>
model: gpt-4o-mini
Hermes keeps conversation history in OpenAI format and dispatches the returned tool_calls through its own tool registry — Visa Key only needs to let the model see the tool schema and return the call, which this endpoint does.
OpenClaw routes cloud models through an OpenAI-compatible client, so point its model endpoint at Visa Key:
OPENAI_BASE_URL=https://auth.visacli.sh/v1
OPENAI_API_KEY=<your-visa-key>
# model: gpt-4o-mini (or any or-* catalog id)
OpenClaw's inference then bills through the Visa Key's prepaid balance and caps.
{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1719072600,
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hello! How can I help you today?", "tool_calls": null },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 9, "completion_tokens": 9, "total_tokens": 18 }
}
Visa billing metadata is returned in response headers:
| Header | Meaning |
|---|---|
X-Visa-Key-Idempotency-Key |
Idempotency key used (client-supplied or server-synthesized). |
X-Visa-Key-Tool |
Resolved Visa catalog tool id (e.g. or-gpt-4o-mini). |
X-Visa-Key-Idempotent-Replay |
Present when replaying an identical prior request. |
X-Visa-Key-Charged-Micros |
Amount charged, in micros (USD x 1,000,000). |
X-Visa-Key-Receipt-Id |
Receipt id for the charge. |
X-Visa-Key-Support-Id |
Support correlation id. |
Streaming is not supported yet. stream: true returns 400. Set stream: false (the default).
Errors on this endpoint use the OpenAI envelope { "error": { "message", "type", "param", "code" } }, not the Visa error_code envelope used by the rest of this API. A not-allowed model is surfaced as 404 model_not_found (mirroring OpenAI, so a key's allowlist never leaks which models exist); rate limits and spend-cap denials map to 429 rate_limit_error (honor Retry-After).
{
"error": {
"message": "The model `gpt-4o` does not exist or you do not have access to it.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}
The model field on /v1/chat/completions accepts a Visa catalog id directly (e.g. or-gpt-4o-mini) or a common alias that resolves to one. An unknown, non-chat, or not-allowed model returns 404 model_not_found.
| Alias(es) | Resolves to |
|---|---|
gpt-4o, openai/gpt-4o |
or-gpt-4o |
gpt-4o-mini, openai/gpt-4o-mini |
or-gpt-4o-mini |
claude-3-5-sonnet, anthropic/claude-sonnet |
or-claude-sonnet |
claude-3-5-haiku, anthropic/claude-haiku |
or-claude-haiku |
claude-3-opus, anthropic/claude-opus |
or-claude-opus |
gemini-3-flash, google/gemini-3-flash |
or-gemini-3-flash |
gemini-3-pro, google/gemini-3-pro |
or-gemini-3-pro |
llama-3-70b, meta-llama/llama-3-70b |
or-llama-70b |
mistral-large, mistralai/mistral-large |
or-mistral-large |
deepseek-chat, deepseek/deepseek-chat |
or-deepseek-chat |
A key's allowed_tools restricts which of these it can call — a model outside the allowlist is reported as model_not_found, not a permission error.
A hosted MCP server over HTTP (MCP Streamable HTTP transport) so a remote MCP client — Cursor, Replit, an agent runtime, Claude Desktop via a remote bridge — can discover and run Visa tools without the local CLI. It authenticates with a Visa Key and wraps the same paid execution path: identical caps, rate limits, idempotency, and telemetry. It is mounted at the auth host root — https://auth.visacli.sh/mcp, not under /v1.
/mcp when your client already speaks MCP and you want tool discovery plus execution over one connection. For a plain HTTP call use POST /v1/api/tools/:tool/execute; for an LLM chat client use /v1/chat/completions. All three share the same key, prepaid balance, and caps.Only two tools are exposed remotely. Key management, webhooks, session budgets, and chat completions are not on the MCP surface — use their REST routes for those.
| Tool | What it does | Proxies to | Charge |
|---|---|---|---|
discover_tools |
Search or list the Visa catalog. Optional query, category, limit (default 100, max 200). |
GET /v1/catalog |
free |
execute_tool |
Run a catalog tool by id. Required tool; all tool params nested under input. |
POST /v1/api/tools/:tool/execute (in-process) |
key balance |
GET /mcp returns human-readable server info — name, version, transport, and the exposed tool list. No authentication required; handy as a connectivity check.
{
"name": "visa-cli",
"version": "3.0.0",
"description": "Visa CLI MCP server — AI tools, payments, crypto prices",
"transport": "streamable-http",
"auth": "X-Api-Key (Visa Key)",
"docs": "https://visacli.sh/docs",
"tools": [ { "name": "discover_tools" }, { "name": "execute_tool" } ]
}
POST /mcp is the MCP Streamable HTTP transport. It is stateless — no session id; each POST is a self-contained JSON-RPC message and returns a JSON response. Auth runs once at this boundary, then execute_tool runs the paid path in-process (your real client IP and key are preserved). Authenticates with a Visa Key.
| Header | Required | Notes |
|---|---|---|
X-Api-Key |
yes | The VisaKey_... secret. Missing returns 401 AUTH_REQUIRED (Visa error envelope, before the transport runs). |
Content-Type |
yes | application/json |
Accept |
recommended | application/json, text/event-stream — Streamable HTTP clients send this automatically. |
Connect any remote-MCP-capable client by pointing it at the URL and passing the key as a header:
{
"mcpServers": {
"visa-cli": {
"url": "https://auth.visacli.sh/mcp",
"headers": { "X-Api-Key": "VisaKey_..." }
}
}
}
List tools (free):
curl -sS https://auth.visacli.sh/mcp \
-H "X-Api-Key: $VISA_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# → result.tools = [ discover_tools, execute_tool ]
Execute a tool:
curl -sS https://auth.visacli.sh/mcp \
-H "X-Api-Key: $VISA_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "execute_tool",
"arguments": {
"tool": "or-gpt-4o-mini",
"input": { "messages": [{ "role": "user", "content": "Say hello." }] }
},
"_meta": { "progressToken": "greeting-op-1" }
}
}'
The result is wrapped in the standard MCP tool-result envelope — the Visa tool_execution payload is JSON-encoded inside result.content[0].text. A failed call returns the same envelope with isError: true and an { "error": ... } text body (an unknown tool name becomes "Unknown tool: ..."). Auth failures are the exception — a missing key returns the Visa 401 AUTH_REQUIRED envelope before the JSON-RPC layer is reached.
Idempotency-Key directly. The server derives a retry-stable key from the client's _meta.progressToken when present — reuse the same progressToken across a retry of the same logical call and it dedupes instead of double-charging. With no progressToken each call mints a fresh key (no dedup). The JSON-RPC message id is deliberately not used, because clients reissue it on retry.Async tools (music, video, 3D, some audio) return a queued job — a request_id plus model_path, not the finished media. Call execute_tool again with tool check_fal_status and input { request_id, model_path } copied verbatim. check_fal_status is free; poll it no more than once every 30s (jobs typically finish in 60–180s). See Capability Examples.
One key reaches 40+ paid tools across image, music, audio, video, 3D, language, and speech. Call any of them through POST /v1/api/tools/:tool/execute: put the tool id in the path and its parameters under input. The live set, exact prices, and exact schemas are always in GET /v1/catalog (public, no key required) — treat it as the source of truth rather than hardcoding this table.
Pricing models. fal-* tools are flat-priced per call (the cents below). or-* tools — the language models and the Gemini image models — are usage/token-metered, so the exact charge is whatever comes back in usage.charged_micros. Either way the charge is metered server-side and capped by the key.
Do not infer parameters from prose — every tool's contract is machine-readable. GET /v1/catalog returns each tool's exact request and response shape. Read three fields and the request is fully determined:
| Catalog field | Tells you |
|---|---|
inputSchema |
Exactly what goes under input — JSON Schema with required, types, default, and min/max. Allowed values for string params are stated in each property's description. |
extractionMode |
The exact JSON path to the output inside result — e.g. images[0].url, choices[0].message.content, text, or request_id (async). |
resultKind |
Sync vs async — image/llm/transcription return inline; any queued_* returns a job to poll. |
Worked example — schema to request to extraction:
{
"id": "fal-flux-pro",
"inputSchema": {
"type": "object",
"required": ["prompt"],
"properties": {
"prompt": { "type": "string", "description": "Text description of the image" },
"aspect_ratio": { "type": "string", "default": "16:9", "description": "Aspect ratio (e.g. 16:9, 1:1, 9:16)" }
}
},
"extractionMode": "images[0].url",
"resultKind": "image",
"modelPath": "fal-ai/flux-pro/v1.1"
}
# the execute body that schema implies
-d '{"input":{"prompt":"a red bicycle leaning on a wall","aspect_ratio":"1:1"}}'
# read the output at extractionMode: response.result.images[0].url
inputSchema types exactly — fal-kling-video duration is a string ("5"/"10"), but fal-ace-step-music duration is a number (60). String "enums" live in the property description, not as JSON-Schema enum (e.g. fal-recraft-v3 style is one of realistic_image, digital_illustration, vector_illustration, icon). A malformed body returns 400 INVALID_REQUEST with the validation reason — correct it and retry with a new Idempotency-Key.All take a prompt; most accept an optional aspect_ratio (default 16:9). The or-gemini-nano-banana models also accept an image_url for edits.
| Tool id | Price | Best for |
|---|---|---|
or-gemini-nano-banana |
metered | Gemini Flash image plus edits |
or-gemini-nano-banana-2 |
metered | Faster Gemini Flash image |
or-gemini-nano-banana-pro |
metered | Gemini Pro image, pro quality |
fal-flux-schnell |
$0.01 | Fastest, ultra-cheap drafts |
fal-fast-sdxl |
$0.01 | 4-step Stable Diffusion |
fal-flux-dev |
$0.03 | Open-source, experimentation |
fal-flux-pro |
$0.04 | Balanced speed and quality |
fal-flux-pro-ultra |
$0.06 | Highest-quality photorealism |
fal-recraft-v3 |
$0.05 | Design, illustrations, icons (style) |
fal-ideogram-v2-turbo |
$0.05 | Fast text rendering |
fal-ideogram-v2 |
$0.08 | Text in images — logos, posters |
These tools are queued (resultKind: queued_*). The execute call returns immediately with a job reference (model_path plus request_id); poll the free check_fal_status tool until the media URL appears.
| Tool id | Price | What it does | Required input |
|---|---|---|---|
fal-ace-step-music |
$0.02 | Full music tracks, vocals or instrumental | prompt (+ duration, instrumental) |
fal-stable-audio |
$0.04 | Sound effects and ambient audio | prompt (+ seconds_total) |
fal-minimax-video |
$0.15 | High-coherence video generation | prompt |
fal-kling-video |
$0.20 | Cinematic text-to-video | prompt (+ duration, aspect_ratio) |
fal-kling-i2v |
$0.35 | Animate a still image into a 5–10s clip | image_url, prompt |
fal-trellis-3d |
$0.08 | Generate a 3D model from a photo | image_url |
All accept prompt (plus optional max_tokens, temperature, system_prompt); they are token-metered. The same models power /v1/chat/completions via aliases. Headline picks (see GET /v1/catalog for the full list):
| Tool id | Notes |
|---|---|
or-gpt-4o-mini |
Fast, affordable everyday GPT-4o |
or-gpt-4o |
Flagship multimodal reasoning |
or-claude-haiku |
Fastest Claude, quick tasks |
or-claude-sonnet |
Best balance — coding and analysis |
or-claude-opus |
Most capable, long context |
or-gemini-3-pro, or-gemini-3-flash |
Google flagship / high-speed |
or-deepseek-r1 |
Chain-of-thought reasoning |
or-perplexity-sonar |
Web-search answers with citations |
or-llama-70b, or-mistral-large, or-qwen-72b |
Strong open models |
| Tool id | Price | What it does | Required input |
|---|---|---|---|
fal-whisper |
$0.02 | Transcribe audio to text | audio_url (+ language) |
fal-metavoice |
$0.03 | Voice-cloning TTS (async) | text, audio_url |
fal-aura-sr |
$0.03 | 4x image upscale | image_url |
check_fal_status |
free | Poll an async job for completion | model_path, request_id |
Concrete execute calls for the headline use cases. Every paid call needs X-Api-Key plus a fresh Idempotency-Key; all tool parameters go under input.
Generate an image:
curl -sS https://auth.visacli.sh/v1/api/tools/or-gemini-nano-banana/execute \
-H "X-Api-Key: $VISA_KEY" \
-H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-H "Content-Type: application/json" \
-d '{"input":{"prompt":"a neon cyberpunk cityscape at night, cinematic","aspect_ratio":"16:9"}}'
The image URL is at this tool's extractionMode (read it from the catalog rather than assuming — the or-gemini-nano-banana family extracts at result.choices[0].message.images[0].image_url.url, while fal-* image tools extract at result.images[0].url), and the charge is in usage.charged_micros.
Generate music (async, two steps):
# 1 — kick off generation (returns a queued job, not the audio yet)
curl -sS https://auth.visacli.sh/v1/api/tools/fal-ace-step-music/execute \
-H "X-Api-Key: $VISA_KEY" \
-H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-H "Content-Type: application/json" \
-d '{"input":{"prompt":"upbeat lo-fi hip hop, mellow piano","duration":30,"instrumental":true}}'
# → result contains { "model_path": "...", "request_id": "..." }
# 2 — poll until the audio URL appears (check_fal_status is free)
curl -sS https://auth.visacli.sh/v1/api/tools/check_fal_status/execute \
-H "X-Api-Key: $VISA_KEY" \
-H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-H "Content-Type: application/json" \
-d '{"input":{"model_path":"fal-ai/ace-step/prompt-to-audio","request_id":"<request_id from step 1>"}}'
model_path is the originating tool's catalog modelPath (here fal-ai/ace-step/prompt-to-audio); request_id comes from step 1's response. Repeat step 2 until the audio URL appears. Video (fal-kling-video, fal-minimax-video) and 3D (fal-trellis-3d) follow the same queue-then-poll pattern.
Run a language model:
curl -sS https://auth.visacli.sh/v1/api/tools/or-claude-sonnet/execute \
-H "X-Api-Key: $VISA_KEY" \
-H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-H "Content-Type: application/json" \
-d '{"input":{"prompt":"Summarize the French Revolution in 3 bullets.","max_tokens":300}}'
For chat-style multi-turn or tool-calling, prefer the OpenAI-compatible endpoint. The catalog tool also accepts OpenAI-style messages under input in addition to prompt.
Transcribe audio:
curl -sS https://auth.visacli.sh/v1/api/tools/fal-whisper/execute \
-H "X-Api-Key: $VISA_KEY" \
-H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-H "Content-Type: application/json" \
-d '{"input":{"audio_url":"https://example.com/clip.mp3","language":"en"}}'
Both execution surfaces are paid POSTs, so they support idempotency keys to make retries safe.
409 IDEMPOTENT_REPLAY (not retryable; generate a fresh key).409 IDEMPOTENCY_IN_FLIGHT (retryable after Retry-After).On /v1/chat/completions the Idempotency-Key header is optional — if omitted the server synthesizes one per request (single-request dedupe) and returns it in X-Visa-Key-Idempotency-Key. But a synthesized key can't protect a retry your client makes after a network-ambiguous failure, because your client never saw it.
Idempotency-Key. Most OpenAI SDKs accept it as a per-request option (idempotency_key / extra_headers).Executions are charged from the key owner's prepaid balance, metered in micros (USD x 1,000,000).
| Control | Behaviour |
|---|---|
| Daily cap | Per-key rolling 24h limit (daily_cap_cents, default 500 = $5/day). |
| Total cap | Optional lifetime ceiling (total_cap_cents). |
| Where the charge shows | Direct execution: usage.charged_cents / usage.charged_micros. Chat: X-Visa-Key-Charged-Micros header. |
Exceeding a cap returns 429 SPEND_LIMIT_EXCEEDED with limit_type of daily_cap or total_cap. This is not retryable — raise the cap, mint a higher-cap key, or wait for the rolling window. Do not auto-retry the identical request.
Per Visa Key, shared across direct execution, chat completions, and remote MCP.
| Limit | Value | On breach |
|---|---|---|
| Request rate | 120 requests / 60s sliding window (RateLimit-Policy: 120;w=60) |
429 RATE_LIMITED (retryable; honor Retry-After) |
| Concurrency | 1 in-flight execution per key | 429 KEY_CONCURRENT_EXECUTION_LIMIT_EXCEEDED |
Responses carry standard RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, and RateLimit-Policy headers so you can self-pace before hitting the 429.
Visa-native routes return a structured JSON envelope. Branch on retryable, error_code, and Retry-After — never on the human message text. (/v1/chat/completions uses the OpenAI error shape instead; see its Errors section.)
{
"success": false,
"error": "human-readable message",
"error_code": "INVALID_REQUEST",
"retryable": false,
"retry_after": 5
}
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST |
Missing or malformed input, idempotency key, pagination, or unsupported fields. |
| 401 | AUTH_REQUIRED |
Missing credential. |
| 401 | AUTH_INVALID |
Credential not recognized. |
| 401 | KEY_ENVIRONMENT_MISMATCH |
Key used against the wrong environment (prod vs preview). |
| 401 | KEY_EXPIRED |
Key is past expires_at. |
| 403 | KEY_SOURCE_IP_DENIED |
Caller IP is outside the CIDR allowlist. |
| 403 | TOOL_NOT_PERMITTED |
Tool is outside the key's allowlist or scope. |
| 403 | TEST_KEY_NOT_SUPPORTED |
A vk_test_ key was used on a live execution surface. |
| 403 | ACCOUNT_NOT_APPROVED |
Key owner is not approved. |
| 404 | TOOL_NOT_FOUND |
Tool id or alias does not resolve. |
| 404 | KEY_NOT_FOUND |
No key with that id for this owner. |
| 409 | IDEMPOTENT_REPLAY |
Same idempotency key reused with a different payload. Not retryable. |
| 409 | IDEMPOTENCY_IN_FLIGHT |
Original request still running. Retry the same request with the same key. |
| 409 | SPEND_EXECUTION_NOT_RESERVABLE |
The spend path could not reserve this execution. Check the tool's live status in GET /v1/catalog. |
| 410 | VISA_KEY_IMMUTABLE |
Edit / rotate routes are retired. Revoke and create a new key. |
| 429 | RATE_LIMITED |
Request-rate limit hit (120/60s). Retryable — wait for Retry-After. |
| 429 | KEY_CONCURRENT_EXECUTION_LIMIT_EXCEEDED |
Another execution is in flight for this key (limit 1). |
| 429 | SPEND_LIMIT_EXCEEDED |
Daily or total cap would be exceeded. Not retryable; limit_type is daily_cap or total_cap. |
| 500 | INTERNAL_ERROR |
Server failure. Retryable. |
| 503 | RATE_LIMITER_UNAVAILABLE |
Rate-limiter store briefly unavailable (not a throttle). Retryable. |
| 503 | IDEMPOTENCY_UNAVAILABLE |
Retryable store outage, or non-retryable reconcile-required state. Check retryable. |
| 503 | SPEND_CAP_STORE_UNAVAILABLE |
Cap store briefly unavailable. Retryable — no charge occurred. |
| 503 | SURFACE_DISABLED |
Kill switch engaged for the surface. |
For retryable errors, wait for retry_after or Retry-After, then resend the identical request with the same Idempotency-Key. For non-retryable errors, change the request or reconcile using the returned support and receipt fields before trying again.
The visa-cli keys commands handle the session-authenticated key lifecycle for you.
# Create a scoped key (secret printed once)
visa-cli keys create my-demo-app --tools fal-flux-pro,or-gpt-4o-mini --daily-cap 5 --total-cap 200
# List your keys
visa-cli keys list
# Revoke a key by id
visa-cli keys revoke 1
| Flag | Maps to |
|---|---|
--tools a,b,c |
allowed_tools (sets tool_scope: restricted) |
--daily-cap N |
daily_cap_cents (dollars to cents) |
--total-cap N |
total_cap_cents (dollars to cents) |