# Visa Key API

# Visa Key API

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:

```bash
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.

## Base URLs

| 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`.

## Create A Key

CLI:

```bash
visa-cli keys create my-demo-app --tools or-gpt-4o-mini --daily-cap 5 --total-cap 200
```

HTTP:

```bash
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:

```json
{
  "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"
}
```

## Execute A Tool

Every direct paid execution requires both `X-Api-Key` and an `Idempotency-Key`.

Route:

```http
POST /v1/api/tools/:tool/execute
```

```bash
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:

```json
{
  "success": true,
  "object": "tool_execution",
  "tool": "or-gpt-4o-mini",
  "result": {},
  "usage": {
    "charged_cents": 1,
    "charged_micros": "10000"
  },
  "receipt": null
}
```

## List And Revoke Keys

CLI:

```bash
visa-cli keys list
visa-cli keys revoke 123
```

HTTP list:

```bash
curl -sS "https://auth.visacli.sh/v1/api/keys?limit=25" \
  -H "Authorization: Bearer $VISA_SESSION_TOKEN"
```

List responses include cursor metadata:

```json
{
  "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:

```bash
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:

```json
{ "success": true, "revoked": 123 }
```

## Update And Rotate

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 And Retries

Errors use a structured JSON envelope:

```json
{
  "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.
