> ## Documentation Index
> Fetch the complete documentation index at: https://docs.valarhq.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Automate coding key provisioning

> Use an organization OAuth credential to create, list, suspend, and revoke ValarCode keys for engineers

<Badge color="green">Private Preview</Badge>

Organization admins can give an internal provisioning service an OAuth credential for managing
ValarCode keys. The credential is limited to the organization that created it and can manage only
keys created through this automation flow.

<Note>
  Client secrets and generated ValarCode keys stay valid until you revoke them. OAuth access tokens
  expire after one hour; request another token with the same client credentials when needed.
</Note>

## Create a credential

1. Open **Settings → ValarCode settings → Automation credentials** in the Valar dashboard.
2. Click **Create credential**.
3. Store the client secret immediately. It is shown only once.
4. Copy the client ID and token endpoint shown beside it.

You can create additional client secrets for rotation. Deploy the new secret before revoking the old
one.

## Provision a key with Python

Install Requests:

```bash theme={"system"}
python -m pip install requests
```

Provide the credential through your secret manager or environment. Do not commit the client secret.

```bash theme={"system"}
export VALAR_M2M_CLIENT_ID="client_..."
export VALAR_M2M_CLIENT_SECRET="..."
export VALAR_M2M_TOKEN_ENDPOINT="https://signin.valarhq.ai/oauth2/token"
export VALAR_ENGINEER_EMAIL="engineer@example.com"

# Optional: copy the routing policy from an existing coding key.
export VALAR_ROUTING_SOURCE_KEY_ID="key_..."
```

To find the source ID, open **ValarCode → Setup**, select the configured key under **Model
routing**, and copy the **Key ID for the automation API** shown below the selector. The `vlrcode_...`
value is only the key's display prefix and cannot be used as `source_key_id`.

Exchange the credential for an access token, then create the engineer's key:

```python theme={"system"}
import os

import requests

API_BASE_URL = os.getenv("VALAR_API_BASE_URL", "https://api.valarhq.ai")
CLIENT_ID = os.environ["VALAR_M2M_CLIENT_ID"]
CLIENT_SECRET = os.environ["VALAR_M2M_CLIENT_SECRET"]
TOKEN_ENDPOINT = os.environ["VALAR_M2M_TOKEN_ENDPOINT"]
ENGINEER_EMAIL = os.environ["VALAR_ENGINEER_EMAIL"]

token_response = requests.post(
    TOKEN_ENDPOINT,
    data={
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    },
    timeout=15,
)
token_response.raise_for_status()
access_token = token_response.json()["access_token"]

headers = {"Authorization": f"Bearer {access_token}"}
workspace_id = os.getenv("VALAR_WORKSPACE_ID")
if workspace_id:
    headers["X-Workspace-Id"] = workspace_id

payload = {"email": ENGINEER_EMAIL}
source_key_id = os.getenv("VALAR_ROUTING_SOURCE_KEY_ID")
if source_key_id:
    payload["routing_policy"] = {"source_key_id": source_key_id}

response = requests.post(
    f"{API_BASE_URL}/v1/organization/keys",
    headers=headers,
    json=payload,
    timeout=15,
)
response.raise_for_status()
created = response.json()

print(f"Created {created['id']} for {created['email']}")
print(f"ValarCode key (shown once): {created['token']}")
```

The routing source key must belong to the selected workspace and already have a routing policy. If
you omit `VALAR_WORKSPACE_ID`, Valar uses the organization's default workspace.

Store the returned `token` immediately. Later list calls return the key ID, prefix, state, and
creation time, but never the full key again.

## Update an engineer's routing

Keep one coding key per routing policy as a template, for example `template: open_weights` on Auto
routing and `template: frontier_and_open` with a manual mapping that includes Claude models, and copy a template onto an engineer's
live key whenever their policy changes. The engineer keeps the same key and nothing is
redistributed; the gateway serves the new routing within about a minute.

```python theme={"system"}
response = requests.put(
    f"{API_BASE_URL}/v1/organization/keys",
    headers=headers,
    params={"email": ENGINEER_EMAIL},
    json={"routing_policy": {"source_key_id": os.environ["VALAR_FRONTIER_TEMPLATE_KEY_ID"]}},
    timeout=15,
)
response.raise_for_status()
```

The response is the key record without its token, including `routing.version` and
`routing.changed_at` for the routing it just wrote. Record the version: a later list showing a higher
version means the routing was changed again, for example by hand in the dashboard. `404 Not Found`
means the email has no live automation-managed key in the selected workspace.

The request is idempotent: repeating it with the same template returns `200 OK` and changes nothing.
It honors `X-Workspace-Id` like the other key endpoints, and the template must live in the same
workspace as the engineer's key.

Copies are snapshots. Editing a template later does not move the engineers already on it; send the
same request again to roll the change out. The gateway refreshes routing about every 30 seconds per
instance, so for up to half a minute after a change requests can still see either policy. Every copy
appears in the routing change history under **Model routing → Change history** with the template's
name as the reason and "Automation" as the actor. To move the engineer back, copy the Auto
template the same way.

A manual mapping applies to requests that name a Claude model (`claude-opus-5-5`, `claude-opus-5`,
`claude-sonnet-5-5`, `claude-sonnet-5`, `claude-haiku-4-5`, `claude-fable-5-1`). A request for the Auto model id (`valar-auto`) is always
served by Auto routing, whatever the key's mapping, because that id means "let Valar pick". To move
an engineer onto a manual mapping, their coding tool must be set to a Claude model, not to Auto.

## List provisioned keys

Use the same `headers` from the example above:

```python theme={"system"}
response = requests.get(
    f"{API_BASE_URL}/v1/organization/keys",
    headers=headers,
    params={"email": ENGINEER_EMAIL},  # Omit params to list every automation-managed key.
    timeout=15,
)
response.raise_for_status()

for key in response.json():
    routing = key.get("routing")  # Absent until a routing policy has been saved for the key.
    print(key["id"], key["email"], "revoked" if key["revoked"] else "active",
          routing and f"routing v{routing['version']} since {routing['changed_at']}")
```

Add `include=templates` to also list the workspace's live console-made keys, so your automation can
resolve a template by name instead of storing its id:

```python theme={"system"}
response = requests.get(
    f"{API_BASE_URL}/v1/organization/keys",
    headers=headers,
    params={"include": "templates"},
    timeout=15,
)
response.raise_for_status()

templates = {k["name"]: k["id"] for k in response.json() if k["provisioning_source"] == "dashboard"}
frontier_template_id = templates["template: frontier_and_open"]
```

Template rows carry `name`, `id`, `prefix`, and `routing`, never a token. Key ids never change:
updating a key's routing, from the API or the dashboard, writes a new routing version under the same
id. Only revoking and recreating a key produces a new id.

## Read a key's usage

The `id` of a provisioned key is the `key_id` the [ValarCode analytics
endpoints](/usage-endpoints#valarcode-analytics) report on and filter by. Those endpoints accept
the same automation credential (the `/v1/usage/coding/*` routes only; the organization-wide
billing endpoints still need a Valar API key), so the provisioning service can read cost, input
and output tokens, and the served-model mix per engineer with the `headers` it already has:

```python theme={"system"}
usage = requests.get(
    f"{API_BASE_URL}/v1/usage/coding/models",
    headers=headers,
    params={"key_id": created["id"], "start": "2026-08-01", "end": "2026-08-31"},
    timeout=15,
)
usage.raise_for_status()
for model in usage.json()["models"]:
    print(model["served_model"], model["input_tokens"], model["output_tokens"], model["spend"] / 100)
```

## Suspend and restore an engineer's key

Disabling a key is the reversible alternative to revoking it. The key keeps its id, its routing,
and the token the engineer already has; it just refuses every request until you enable it again.
Use it for leave, an offboarding hold, or a spend investigation, where revoke-and-remint would
force a new token onto the engineer's machine.

```python theme={"system"}
response = requests.patch(
    f"{API_BASE_URL}/v1/organization/keys",
    headers=headers,
    params={"email": ENGINEER_EMAIL},
    json={"enabled": False},   # True to restore
    timeout=15,
)
response.raise_for_status()
print(response.json()["enabled"])  # False
```

The response is the key record without its token. Every list row also carries `enabled`, so a
periodic reconciliation can tell suspended engineers from active ones without keeping state of
its own. `404 Not Found` means the email has no live automation-managed key in the selected
workspace. The request is idempotent: disabling a disabled key, or enabling an enabled one,
returns `200 OK` and changes nothing.

While a key is disabled, the engineer's coding tool receives `403 Forbidden` with the message
`this API key is disabled — ask your organization admin to re-enable it`, not the `401` an
unknown key gets, so they know who to ask. The gateway checks the switch on every request; there
is no propagation delay. Usage recorded before the suspension stays attributed to the key, and
the dashboard shows the key as **Disabled** in both key lists.

A revoked key cannot be disabled or re-enabled: revocation is permanent, and a `PATCH` on an
email whose only key is revoked returns `404`.

## Revoke an engineer's key

Revocation is explicit and immediate:

```python theme={"system"}
response = requests.delete(
    f"{API_BASE_URL}/v1/organization/keys",
    headers=headers,
    params={"email": ENGINEER_EMAIL},
    timeout=15,
)
response.raise_for_status()
```

Create requests return `409 Conflict` when that email already has a live automation-managed key in
the selected workspace. Revoke the existing key before creating its replacement.
