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.
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.
| Environment | Base URL |
|---|---|
| Production | https://auth.visacli.sh |
| Staging or preview | https://auth-visa-code-preview.up.railway.app |
Visa Keys are environment-scoped. A preview key used against production, or a production key used against preview, returns 401 KEY_ENVIRONMENT_MISMATCH.
CLI:
visa-cli keys create my-demo-app --tools or-gpt-4o-mini --daily-cap 5 --total-cap 200
HTTP:
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"
}'
Important fields:
| Field | Required | Notes |
|---|---|---|
label |
yes | Human-readable key name. |
allowed_tools |
no | Array of tool ids. Omit for all supported tools, or provide at least one id with tool_scope: "restricted". |
tool_scope |
no | restricted or all_supported_tools. Restricted keys must have at least one allowed tool. |
daily_cap_cents |
no | Daily key cap in cents. Values are clamped to the supported range. |
total_cap_cents |
no | Cumulative key cap in cents, or null for no cumulative cap. |
allowed_cidrs |
no | Optional CIDR allowlist for source IPs. Malformed CIDRs fail closed. |
expires_at |
no | ISO-8601 timestamp with timezone. Must be in the future. |
Create returns the raw key once:
{
"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"
}
Every direct paid execution requires both X-Api-Key and an Idempotency-Key.
Route:
POST /v1/api/tools/:tool/execute
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."}]}}'
Request shape:
| Field | Required | Notes |
|---|---|---|
:tool |
yes | Tool id or alias in the path. Unknown tools return 404 TOOL_NOT_FOUND. |
input |
yes | Tool-specific parameters. Send {} for tools with no parameters. |
Idempotency-Key |
yes | UUID v4 per logical paid operation. Reuse the same value on retries of that operation. |
Do not send max_cents, dry_run, stream: true, session budget ids, voucher fields, or raw top-level tool parameters. Put provider parameters under input.
Success responses use a stable tool-execution envelope:
{
"success": true,
"object": "tool_execution",
"tool": "or-gpt-4o-mini",
"result": {},
"usage": {
"charged_cents": 1,
"charged_micros": "10000"
},
"receipt": null
}
CLI:
visa-cli keys list
visa-cli keys revoke 123
HTTP list:
curl -sS "https://auth.visacli.sh/v1/api/keys?limit=25" \
-H "Authorization: Bearer $VISA_SESSION_TOKEN"
List responses include cursor metadata:
{
"success": true,
"keys": [],
"limit": 25,
"has_more": false,
"next_cursor": null,
"previous_cursor": null
}
Use starting_after to move to older keys with next_cursor, or ending_before to move back toward newer keys with previous_cursor. Do not send both cursors in the same request.
HTTP revoke:
curl -sS -X DELETE https://auth.visacli.sh/v1/api/keys/123 \
-H "Authorization: Bearer $VISA_SESSION_TOKEN"
Revoked keys can no longer execute tools. Revoke returns:
{ "success": true, "revoked": 123 }
The HTTP API also supports key updates and rotation for control-plane clients:
| Method | Route | Purpose |
|---|---|---|
PATCH |
/v1/api/keys/:id |
Update label, tool scope, allowed tools, CIDRs, daily cap, or total cap. |
POST |
/v1/api/keys/:id/rotate |
Mint a replacement raw key and revoke the previous secret. |
Rotation returns the replacement raw key once. Store it immediately, deploy it to the consuming service, then remove the old secret from that service.
Errors use a structured JSON envelope:
{
"success": false,
"error": "human-readable message",
"error_code": "INVALID_REQUEST",
"retryable": false,
"retry_after": 5
}
Branch on retryable, error_code, and Retry-After; do not parse message text.
| 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. |
| 403 | 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 allowlist or key scope is invalid. |
| 403 | ACCOUNT_NOT_APPROVED |
Key owner is not approved. |
| 404 | TOOL_NOT_FOUND |
Tool id or alias does not resolve. |
| 409 | IDEMPOTENT_REPLAY |
Same idempotency key was reused with a different payload. |
| 409 | IDEMPOTENCY_IN_FLIGHT |
Original request is still running. Retry the same request with the same key. |
| 429 | RATE_LIMITED |
Rate limit or key/account cap reached. |
| 503 | IDEMPOTENCY_UNAVAILABLE |
Retryable store outage, or non-retryable reconcile-required state. Check retryable. |
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.