Use the Viktor Public API to give Viktor work from your own applications and automations. The API is asynchronous: create a thread with a message, track the resulting run, then fetch the final result.

- **Base URL:** all endpoints live under `https://api.viktor.com/api/public/v1`.
- **Plan:** API keys are available on paid plans and Enterprise. On the free plan, [Settings → API Keys](https://app.viktor.com/settings/api-keys) shows **API access is a paid feature** with an **Upgrade** button instead of the key list.
- **Credits:** API runs consume workspace credits like any other Viktor task. See [how credits work](/help/category/credits/how-credits-work) and [pricing](/pricing).
- **MCP:** Prefer the [Viktor MCP server](/docs/mcp-server) when your caller is an MCP-compatible agent.
- **OpenAI or Anthropic SDKs:** Use the [compatible chat API](/docs/chat-completions) to drive Viktor with existing SDK code.

## Quickstart

### 1. Create an API key

API keys are available on paid plans and Enterprise. If your workspace is on the free plan, [upgrade](https://app.viktor.com/settings/subscription/plans) first.

Open [Settings → API Keys](https://app.viktor.com/settings/api-keys), select **Generate Key**, and grant only the scopes your integration needs. Personal keys act as you. Team keys are for shared automations and can be created by team admins.

The secret is shown once. Store it in a secret manager and never put it in source control or client-side code. Keys begin with `zt_live_sk_`.

### 2. Check the key

Send the key as a Bearer token. The `x-api-key` header is also supported.

```bash
export VIKTOR_API_KEY="zt_live_sk_..."

curl "https://api.viktor.com/api/public/v1/me" \
  -H "Authorization: Bearer $VIKTOR_API_KEY"
```

`GET /api/public/v1/me` returns the key type, granted scopes, the workspace's rate-limit tier, and the active rate-limit policies scaled to that tier. It requires no scope, so it is the best first request after creating or rotating a key.

### Manage keys

All key management happens in [Settings → API Keys](https://app.viktor.com/settings/api-keys):

- **Edit** a key's label or scopes. The owner of a personal key and team admins for team keys can edit.
- **Rotate** an active key to replace it with a new key that keeps its label and scopes. The new secret is shown once, and the old key is revoked at the same moment, so update your callers right away.
- **Revoke** a key to stop it immediately, or **delete** it to remove it from the list. Revoking and deleting keep working after a workspace moves to the free plan, so you can always retire a key you can no longer manage.
- Each person can hold up to 50 active personal keys, and each team up to 50 active team keys.
- Team admins see team keys and their own personal keys. They do not see other members' personal keys.

### 3. Give Viktor a task

Creating a thread also queues its first agent run. Use an idempotency key so a retry cannot start the same task twice.

```bash
curl -X POST "https://api.viktor.com/api/public/v1/threads" \
  -H "Authorization: Bearer $VIKTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: monthly-revenue-2026-07" \
  --data @- <<'JSON'
{
  "message": "Analyze this month's revenue and summarize the biggest changes.",
  "speed": "smarter"
}
JSON
```

The API returns `202 Accepted` with `thread.id`, `message.id`, and `run.id`. Save the run ID.

`speed` selects the agent configuration: `smarter` (the default) maximizes quality, while `faster` trades some depth for lower latency on simple tasks.

### 4. Wait for the result

Poll the run until its `status` is terminal: `completed`, `requires_action`, `failed`, `cancelled`, or `timed_out`.

`requires_action` means Viktor finished the run by asking you for something — missing input, a decision, or an approval. The result is available like a completed run: read it, then answer by sending a follow-up message on the same thread.

```bash
curl "https://api.viktor.com/api/public/v1/runs/<run_id>" \
  -H "Authorization: Bearer $VIKTOR_API_KEY"
```

When `result.available` is `true`, fetch the final result:

```bash
curl "https://api.viktor.com/api/public/v1/runs/<run_id>/result" \
  -H "Authorization: Bearer $VIKTOR_API_KEY"
```

Results can contain Markdown, structured JSON, and file artifacts. To continue the same conversation, send another message to the existing thread.

```bash
curl -X POST "https://api.viktor.com/api/public/v1/threads/<thread_id>/messages" \
  -H "Authorization: Bearer $VIKTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: monthly-revenue-follow-up-1" \
  -d '{"message":"Turn the analysis into a one-page executive brief."}'
```

## Endpoint reference

| Method | Endpoint | Purpose | Required scopes |
| --- | --- | --- | --- |
| `GET` | `/api/public/v1/me` | Identify the key and inspect scopes and rate limits | None |
| `GET` | `/api/public/v1/test` | Lightweight authentication check | None |
| `POST` | `/api/public/v1/threads` | Create a thread and start a run | `threads:create`, `runs:create` |
| `GET` | `/api/public/v1/threads` | List threads | `threads:read` |
| `GET` | `/api/public/v1/threads/{thread_id}` | Get a thread | `threads:read` |
| `POST` | `/api/public/v1/threads/{thread_id}/messages` | Continue a thread and start a run | `messages:create`, `runs:create` |
| `GET` | `/api/public/v1/threads/{thread_id}/messages` | List user-visible messages | `messages:read` |
| `GET` | `/api/public/v1/threads/{thread_id}/runs` | List runs in a thread | `runs:read` |
| `GET` | `/api/public/v1/runs/{run_id}` | Get run status | `runs:read` |
| `GET` | `/api/public/v1/runs/{run_id}/result` | Get a terminal run result | `runs:read` |
| `POST` | `/api/public/v1/runs/{run_id}/cancel` | Cancel an in-flight run | `runs:create` |
| `POST` | `/api/public/v1/scripts/runs` | Run a Python script directly in the workspace sandbox | `scripts:execute` |
| `GET` | `/api/public/v1/scripts/runs/{script_run_id}` | Get a script run's status and result | `scripts:execute` |
| `POST` | `/api/public/v1/scripts/runs/{script_run_id}/cancel` | Cancel a queued script run | `scripts:execute` |
| `GET` | `/api/public/v1/integrations` | List connected integrations and their SDK modules | `integrations:read` |
| `GET` | `/api/public/v1/integrations/{sdk_module}/tools` | List an integration's SDK tools with schemas and snippets | `integrations:read` |
| `GET` | `/api/public/v1/files/{file_token}/download-url` | Get a short-lived artifact download URL | `files:read` |
| `GET` | `/api/public/v1/credits` | Get the workspace credit balance and runout forecast | `usage:read` |
| `GET` | `/api/public/v1/usage/summary` | Get windowed credit usage totals by spend bucket | `usage:read` |
| `GET` | `/api/public/v1/usage/daily` | Get per-day credit usage for a window | `usage:read` |
| `GET` | `/api/public/v1/audit/events` | Pull workspace audit events, cursor-paginated (team keys only) | `audit:read` |
| `GET` | `/api/public/v1/audit/digests` | Daily integrity digests of the audit log: per UTC day, the event count, a SHA-256 over the day's event ids (proves none was removed or added) and a SHA-256 over every retained field (also catches an event edited in place under its original id). Team keys only | `audit:read` |

The full interactive reference — every parameter, schema, response body, and a downloadable spec for generated clients — lives in the dashboard under [Settings → API Docs](https://app.viktor.com/settings/api-docs).

## Scopes

- `threads:create` — create new API threads.
- `threads:read` — read and list API threads.
- `messages:create` — create messages in API threads.
- `messages:read` — read messages in API threads.
- `runs:create` — trigger and cancel agent runs.
- `runs:read` — read and list run status and results.
- `scripts:execute` — run Python scripts directly in the workspace sandbox, poll them, and cancel queued script runs.
- `integrations:read` — list connected integrations and read their SDK tools, parameter schemas, and usage snippets.
- `files:read` — download result artifacts.
- `chat:completions` — use the [OpenAI- and Anthropic-compatible chat API](/docs/chat-completions).
- `usage:read` — read workspace-wide credit usage and the credit balance.
- `audit:read` — pull workspace audit events. Team keys only, on Enterprise workspaces with the audit log enabled in workspace settings.

A key with no scopes can only call `/me` and `/test`. Scope changes take effect on new requests immediately.

## Script runs

Sometimes your own code is the orchestrator and you do not want an agent. Script runs execute one Python script directly in your workspace sandbox. Your script keeps full control of the logic, and Viktor supplies the sandbox, the connected integrations, and the file plumbing.

The script runs as the key owner's identity. It sees the same workspace files the same person sees in chat, and its integration calls pass the same permission checks.

### Discover what a script can call

List the connected integrations your key can use and the SDK module each one is imported from:

```bash
curl "https://api.viktor.com/api/public/v1/integrations" \
  -H "Authorization: Bearer $VIKTOR_API_KEY"
```

```json
{
  "integrations": [
    { "sdk_module": "mcp_linear", "name": "Linear", "kind": "mcp", "account_label": null, "tool_count": 12 },
    { "sdk_module": "pd_gmail", "name": "Gmail", "kind": "pipedream", "account_label": "ops@example.com", "tool_count": 31 }
  ]
}
```

Then list one integration's tools. Every tool comes back exactly as the sandbox SDK exposes it: the function name, the parameters with types and required flags, whether a call needs human approval, and a runnable snippet you can submit to `POST /scripts/runs` as-is:

```bash
curl "https://api.viktor.com/api/public/v1/integrations/mcp_linear/tools" \
  -H "Authorization: Bearer $VIKTOR_API_KEY"
```

```json
{
  "name": "linear_create_issue",
  "sdk_module": "mcp_linear",
  "description": "Create a Linear issue.",
  "requires_approval": false,
  "parameters": [
    { "name": "title", "type": "str", "required": true, "description": "Issue title", "default": null }
  ],
  "snippet": "import asyncio\n\nfrom sdk.tools.mcp_linear import linear_create_issue\n\n\nasync def main() -> None:\n    result = await linear_create_issue(\n        title=...,  # str\n    )\n    print(result)\n\n\nasyncio.run(main())\n"
}
```

The discovery set matches execution: a personal key sees the connections its owner can use in chat, and a team key without a chat identity does not see access-restricted connections. Built-in modules (`default_tools`, `utils_tools`, ...) vary per workspace; a script can list `/work/sdk/tools/` to discover them in place.

### Start a run

Start a run with the script source inline. Short scripts can return their result in one round trip with `wait_seconds`:

```bash
curl -X POST "https://api.viktor.com/api/public/v1/scripts/runs" \
  -H "Authorization: Bearer $VIKTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sync-crm-2026-08-28" \
  -d '{
    "code": "print(\"hello from the sandbox\")",
    "timeout_seconds": 120,
    "wait_seconds": 30
  }'
```

The response is the run object. While the run is `queued` or `in_progress`, poll it:

```bash
curl "https://api.viktor.com/api/public/v1/scripts/runs/<script_run_id>" \
  -H "Authorization: Bearer $VIKTOR_API_KEY"
```

A terminal run carries the `result`: the exit code, captured `stdout` and `stderr`, and `artifacts` for output files. Statuses are `queued`, `in_progress`, `completed`, `failed`, `cancelled`, and `timed_out`.

Rules and limits:

- **Output files:** write them to the directory in the `VIKTOR_OUTPUT_DIR` environment variable. Each file comes back as an artifact token for the files endpoint (`files:read` scope). At most 20 files per run, each at most 50 MB.
- **Output text:** `stdout` and `stderr` keep the last 64,000 and 16,000 characters. Write large outputs to the output directory instead.
- **Timeout:** `timeout_seconds` (1–600, default 300) bounds the script's runtime. A script over the limit is stopped and the run becomes `timed_out`.
- **Waiting:** `wait_seconds` (0–120) holds the response until the run is terminal or the wait ends. Longer scripts are polled like agent runs.
- **Cancellation:** only a `queued` run can be cancelled. A script that already executes runs to its end or to its timeout.
- **Arguments:** pass argv values in `args` (at most 16, each at most 2,000 characters). Read them from `sys.argv` in the script.
- **Concurrency:** at most 4 active script runs per key and 8 per workspace.
- **Retries:** use `Idempotency-Key`. A retried create returns the accepted run instead of executing the script twice.

## Workspace usage and credits

The usage endpoints report workspace-wide spend in credits — the same numbers, buckets, and cron attribution as the dashboard's Usage page. Because that data covers the whole workspace, visibility follows the dashboard's admin rule: team keys with `usage:read` qualify as-is (only workspace admins can create them or edit their scopes), while a personal key also requires its owner to currently be a workspace admin — otherwise requests fail with `403 admin_role_required`.

Windows are selected with either a named `period` (`today`, `last_7_days`, `last_30_days`, `this_month`, `last_month`, `current_billing_period` — the default — or `previous_billing_period`) or an explicit `start_time`/`end_time` pair of at most 366 days. Days are bucketed in UTC unless you pass an IANA `timezone`.

`GET /credits` needs no window: it returns the current balance across all credit pools, the billing period, and a burn-rate runout forecast — useful to check before dispatching a batch of runs instead of discovering an empty balance through `402` errors.

## Structured output

Set `response_format` to a JSON Schema when your application needs a predictable object instead of Markdown. Viktor returns the validated value in the result's `json` field. The complete `response_format` schema is in the [API reference](https://app.viktor.com/settings/api-docs).

## Pagination

Thread and run lists use keyset pagination. Pass the last item's ID as `starting_after`. Message lists use `after`.

## Files

Run results return file artifacts as tokens, not permanent public URLs. Exchange an artifact's `id` at `/api/public/v1/files/{file_token}/download-url`. The returned URL is served from Viktor's API domain, requires no further authentication, and is valid for about 15 minutes.

## Rate limits and retries

Limits apply per route and per key owner: all personal keys of the same user share one quota, and all team keys of the same team share one quota, so creating additional keys does not increase throughput.

Limits scale with your workspace plan. Every workspace resolves to a rate-limit tier — `free`, `entry`, `mid`, `high`, or `enterprise` — and higher tiers get proportionally higher limits on every route. Read your tier and the exact values that apply to your keys from `GET /api/public/v1/me` (`rate_limit_tier` and `limits`); a plan change takes effect within a few minutes. Upgrading your plan is the supported way to raise your limits.

Every response includes `X-RateLimit-*` headers with your current quota; a `429` adds `Retry-After` and a `plan_tier` field in the error detail. Wait for the advertised interval before retrying.

Agent runs also have a concurrency cap: at most 8 active runs per key and 32 per workspace. A request over the cap fails with `429 concurrency_limit_exceeded` and a `Retry-After` header; wait for runs to finish or cancel some, then retry. Script runs have their own cap (see [script runs](#script-runs)).

Use `Idempotency-Key` on requests that start runs. Reusing the same key and request returns the originally accepted operation instead of spending credits on a duplicate.

## Errors

Errors use one JSON envelope:

```json
{
  "detail": {
    "error": "machine_readable_code",
    "message": "Human-readable explanation"
  }
}
```

Some errors add fields such as `scope` or `retry_after_seconds`. Treat the machine-readable `error` value as stable application logic and the message as display text.

The exception is a request body or query parameter that does not match the endpoint's schema, such as a message over 100,000 characters or `timeout_seconds` above 600. It returns `422` with `detail` as a list of field errors, each with `loc`, `msg`, and `type`, instead of the envelope.