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

# Use owned keys

> Mint a capped key, inspect what it can do, and share it by QR claim link.

An [owned key](/concepts/owned-keys) is a capped sub-identity you hand to a
teammate, a device, or a machine — it can never out-scope you, and it loses power
the moment your own does. This guide mints one, checks what it may do, and moves it
onto another device with a one-time QR claim link.

## Prerequisites

[Access control](/guides/access-control) is on, and you hold a key whose scopes are
the ceiling for what you can delegate. A non-admin owner may grant only scopes it
holds itself; an admin may also mint ownerless machine keys. Point `TAI_SERVER_URL`
at your server and `TAI_API_KEY` at your key (the CLI reads both from the
environment).

## Mint a capped key

Mint a key owned by you. A non-admin owner may grant only a **subset** of its own
scopes — the new key is automatically owned by the minter, and the raw `sk-…` value
is returned exactly once, so capture it then.

```bash examples/owned_keys/mint_owned_key.sh theme={null}
# An owner mints a capped key. A non-admin owner may grant only scopes it holds
# itself, and the new key is owned by the minter — the raw sk-… value is returned once.
capped=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$TAI_BASE_URL/api/auth/api-keys" \
  -H "X-Api-Key: $TAI_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"user_id": "field-scanner", "description": "read-only field key", "scopes": ["read"]}')

# Asking for a scope the owner does not hold is rejected at mint time: a fixed 400,
# never a silently narrowed key.
excess=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$TAI_BASE_URL/api/auth/api-keys" \
  -H "X-Api-Key: $TAI_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"user_id": "over-reach", "description": "over-scoped key", "scopes": ["read", "write"]}')

echo "capped=$capped excess=$excess"
```

Asking for a scope you do not hold yourself is rejected at mint time with a fixed
`400` — the key is never silently narrowed to fit. From the Studio, the same mint
lives on the API-keys tab of the [Settings](/studio/api-keys) screen, where the
scope picker is capped to your own scopes.

<Info>
  Ownership is exactly one level deep: an owned key can never itself mint keys. Only
  an admin may mint an **ownerless** machine key. See
  [accounts](/concepts/accounts) for who may mint what.
</Info>

## Inspect what the key can do

A scoped key needs to know its own limits. `tai auth whoami` prints the caller's
[capability projection](/concepts/owned-keys) — the routes, tools, and agents it
can reach right now and its effective scopes, plus a `mintable` flag. Run it with
the owned key (set `TAI_API_KEY` to the key you just minted):

```bash examples/owned_keys/whoami.sh theme={null}
set -e
# `tai auth whoami` prints the caller's derived capability projection — the routes,
# tools, and agents this key can reach right now, plus whether it may mint keys.
tai auth whoami

# The same projection straight from the HTTP door the CLI wraps.
curl -sS -H "X-Api-Key: $TAI_API_KEY" "$TAI_BASE_URL/api/auth/me"
```

The projection is derived from the same store and policy the gate reads and is
jq-exact, so it never lists a door the gate would then deny. It is also what the
scoped Studio renders from — a limited key sees only the features it can reach.

The `mintable` flag is not a per-caller grant: it reports whether this
**deployment's** identity provider can mint keys at all (a validator-only provider
reports `false`). An owned key can never mint regardless — it sees `mintable: true`
under a mint-capable provider yet still gets a `403` on any mint, because ownership
is exactly one level deep.

## Watch it die with the owner

An owned key never has to be revoked on its own — it is capped by its owner's
*current* policy on every request, so attenuating the owner attenuates the key on its
very next call. Prove the mechanism end to end: disable the owner, empty the owner's
scopes, or revoke the owner's key, then make any call with the owned key. The gate
re-evaluates the key against the now-empty owner policy and denies it with a `403` —
the key is powerless while the owner is down, and recovers the instant the owner is
restored. (Revoking the owned key itself is the other lever: its own credential then
fails to authenticate at all.) This is why you revoke a person once, rather than
chasing down every key they were ever issued.

## Share it by QR claim link

To move a key onto another device without ever putting the secret in a URL, mint a
**one-time claim link**. Its token rides the URL fragment (`/login#claim=<token>`),
so it never reaches a server log, and the first exchange burns it.

```bash examples/owned_keys/claim_link.sh theme={null}
set -e
# 1. Mint a one-time claim link that carries a key you already hold to another device.
#    The token rides the URL fragment (/login#claim=<token>) and is single-use.
token=$(tai keys claim-link "$TAI_API_KEY" --json \
  | python3 -c 'import json, sys; print(json.load(sys.stdin)["token"])')

# 2. On the other device, exchange the token for the key — a public, credential-free
#    door. `tai auth claim` accepts the bare token or the whole claim URL.
claimed=$(tai auth claim "$token" --json \
  | python3 -c 'import json, sys; print(json.load(sys.stdin)["user_id"])')

# 3. The link is single-use: exchanging the same token again is refused with a uniform
#    404 (a used, unknown, or expired token are indistinguishable to the caller).
if tai auth claim "$token" >/dev/null 2>&1; then second="reused"; else second="refused"; fi

echo "claimed=$claimed second=$second"
```

You may create a claim link only for a key you own (or, as an admin, any key); the
submitted key is validated before the link is stored, so a bad key fails fast rather
than producing a dead QR. Compose the absolute URL — or render a QR — from your own
origin and the returned `claim_path`, then open it on the other device: the
[sign-in page](/studio/login) recognises the `#claim=` fragment and exchanges the
token for the key automatically.

<Warning>
  The link is **single-use** and short-lived. A used, unknown, or expired token all
  answer the same `404` — deliberately indistinguishable, so the public exchange door
  gives an attacker no oracle to probe. There is no way to list or revoke a claim in
  flight: wait out the TTL, or revoke the underlying key (the exchange then refuses to
  hand out a revoked credential).
</Warning>

## Grant scopes safely

Isolation makes some read scopes safe to grant to an owned key and leaves others
operator-only. A restricted key sees only its own tool runs, interactions, and
notifications, so those read scopes are safe. **Never** grant an owned key the
observability, storage, presets, or schedules scopes — those surfaces are
account-wide and unowned. The full table is in the
[owned-keys scope doctrine](/concepts/owned-keys#scope-doctrine).

## See also

* [Owned keys](/concepts/owned-keys) — the delegation model, the projection, and
  isolation.
* [Set up access control](/guides/access-control) — the gate, scopes, and jq
  policies behind the cap.
* [Sign in to the Studio](/studio/login) — the API-key and claim-link sign-in legs.
* [CLI reference](/reference/cli/auth) — `tai auth whoami`, `tai auth claim`, and
  [`tai keys claim-link`](/reference/cli/keys).
