---
title: "Token endpoint (authorization code + device grant)"
method: POST
path: "/oauth/token"
tags: ["OAuth"]
---

# Token endpoint (authorization code + device grant)

`POST /oauth/token`

## Request

- Base URL: `https://api.infolang.ai`
- URL: `https://api.infolang.ai/oauth/token`
- Auth: none declared

Issues an `il_oauth_` access token. `application/x-www-form-urlencoded` per RFC 6749 §4.1.3; JSON is also accepted, because several MCP clients send it. Client authentication (Basic or body, required only for a confidential client) runs FIRST for both grants, before anything is consumed.

### `grant_type=authorization_code`

Then an ATOMIC single-use consumption of the code, then expiry, `client_id`, `redirect_uri` and the PKCE `code_verifier`. The code is consumed BEFORE the later checks on purpose — a code that fails one of them must not remain usable for a better-formed retry.

### `grant_type=urn:ietf:params:oauth:grant-type:device_code`

RFC 8628 §3.4. No PKCE: the flow has none, because the `device_code` is itself the high-entropy secret and it never travels through a browser. Checks run in this order, and the order is the specification: unknown/wrong-client `device_code` → `invalid_grant`; polled faster than `interval` → `slow_down`; past 600s → `expired_token`; refused by the human → `access_denied`; nobody has answered yet → `authorization_pending`; approved → an atomic single-use consume, then mint.

`slow_down` deliberately precedes expiry and status: §3.5 treats it as a rate-limit signal rather than a state report, and a client hammering a dead code should be told to slow down first — otherwise the cheapest way to poll fast is to poll an expired code. A refused poll still updates the clock, so hammering never advances it.

**There are no refresh tokens in v1**: Kairos has nowhere to store one, so a single longer-lived access token is issued instead (90 days by default). Refresh is a follow-up that lands with Kairos's refresh storage.

**Namespace limitation:** OAuth tokens cannot be restricted below the workspace in v1: Kairos's oauth_tokens table has no scoped_namespaces column (only the gateway's key_metadata sidecar does, and that is keyed by api-key key_id). A consented workspace grants every namespace in it.

## Response `200`

The access token (`Cache-Control: no-store`)

- object
  - `access_token` string, required
  - `token_type` 'Bearer', required
  - `expires_in` number, required
  - `scope` string

## Other responses

- `400`: `invalid_request`, `invalid_grant` or `unsupported_grant_type`; on the device grant also `authorization_pending`, `slow_down`, `access_denied` and `expired_token` (RFC 8628 §3.5)
- `401`: `invalid_client` — unknown client or failed client authentication

---

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