---
title: "Exchange the official CLI OAuth access token from `kody login` for a user-owned `kody_at_` device token."
method: POST
path: "/v1/tokens/device"
tags: ["tokens"]
---

# Exchange the official CLI OAuth access token from \`kody login\` for a user-owned \`kody_at_\` device token.

`POST /v1/tokens/device`

## Request

- Base URL: `https://api.kody.codes`
- URL: `https://api.kody.codes/v1/tokens/device`
- Auth: HTTP bearer

Exchange the official CLI OAuth access token from `kody login` for a user-owned `kody_at_` device token. The OAuth bearer is accepted only on this route (and on local-execute HTTP for older CLIs). The token value is returned once. Scopes are the org-permission vocabulary; the caller role still applies. Requires `lifetime` (`short` or `long`) or both idle and max seconds.

Org permission: none (touches no org data).

## Request body

- object
  - `lifetime` 'short' | 'long': Required unless idle_ttl_seconds and max_lifetime_seconds are both set. `short` = 1h idle / 24h max. `long` = 14d idle / 3mo max. Single-task agents should use `short`.
  - `idle_ttl_seconds` integer: Seconds without use before the token expires. Required with max_lifetime_seconds when lifetime is omitted.
  - `max_lifetime_seconds` integer: Absolute lifetime cap in seconds. Required with idle_ttl_seconds when lifetime is omitted.
  - `name` string, required: Label shown in token lists, e.g. `kody-cli on laptop`.

## Response `200`

Success.

- object
  - `id` string, required
  - `name` string, required
  - `org_id` string, required: For org-bound tokens: the fixed org id. For user-owned tokens: the personal org id (placeholder; each request picks an org via `?org=`).
  - `org_binding` 'org' | 'user', required: `org`: fixed to org_id. `user`: person-owned; pass `?org=<slug>` on each request (default personal org). Membership/grants are checked live.
  - `scopes` string[], required
  - `status` 'active' | 'expired' | 'revoked', required
  - `idle_ttl_seconds` integer, required
  - `expires_at` string, required: Sliding expiry. Each successful request pushes it forward.
  - `max_expires_at` string, required: Absolute expiry. The token never outlives it.
  - `created_via` string, required
  - `created_at` string, required
  - `last_used_at` string, nullable, required
  - `rotated_at` string, nullable, required
  - `revoked_at` string, nullable, required
  - `profile_name` string, nullable, required: Named connection profile this token is bound to, or null for unlimited.
  - `token` string, required: The bearer token. Shown once; Kody stores only a hash. Send it as `Authorization: Bearer <token>`.
  - `token_type` 'Bearer', required
  - `login_org` object, nullable, required: Org the CLI OAuth grant was bound to. The device token is user-owned; pass this slug as ?org= on later calls.
    - `slug` string, required

## Other responses

- `400`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `401`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `403`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `404`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `409`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `413`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `429`: Too many requests (edge IP/token limit) or an entitlement / plan-cap limit. Wait at least `Retry-After` seconds before retrying.
- `500`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `503`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `default`: Error envelope `{ "error": { "code", "message", "details"? } }`.

---

[API](https://skmtc.dev/kody/apis/kody-api.md) · [All operations](https://skmtc.dev/kody/apis/kody-api/llms.txt) · [OpenAPI document](https://skmtc.dev/kody/apis/kody-api/revisions/a020ecedb82b?raw)
