---
title: "Approve or deny a device (console only)"
method: POST
path: "/oauth/device/decision"
tags: ["OAuth"]
---

# Approve or deny a device (console only)

`POST /oauth/device/decision`

## Request

- Base URL: `https://api.infolang.ai`
- URL: `https://api.infolang.ai/oauth/device/decision`
- Auth: HTTP bearer

The device flow's `POST /oauth/consent`: same session gate, same attenuation rules, and literally the same consent evaluation code. The difference is structural — there is no redirect, because no browser is waiting. The device learns the outcome by polling.

**A user cannot consent to more than they hold.** The approved `scopes` must be within what the consenter's roles permit on the chosen `workspaces`; `max_role` is a ceiling only, since Kairos takes `min(grant role, max_role)` per workspace.

**Single use, both ways.** The approve/deny transition is an atomic conditional update, so a double-clicked button, a replay, or an approve racing a deny resolves to exactly one outcome and the rest get 404. A denial is terminal too — a human who refuses cannot be talked into retrying a code an attacker still holds.

The approver's IP and User-Agent are recorded alongside the device's, and both appear on the `oauth.device_approved` activity row.

**Device-code phishing is actively exploited.** The attack needs no vulnerability: an attacker starts the flow on their own machine, gets a real `user_code`, and talks a victim into entering it — every URL the victim sees is genuine, and what they approve is the attacker's device. The countermeasures are the implementation's: a 600-second expiry, single-use codes, a five-attempt entry ceiling per console principal, and an IP/User-Agent record of BOTH legs on the grant so a mismatch is visible to anomaly review.

## Request body

- object
  - `user_code` string, required
  - `deny` boolean
  - `workspaces` string[]
  - `scopes` string[]
  - `max_role` 'Owner' | 'Admin' | 'Member' | 'ReadOnly'

## Response `200`

The decision that was recorded

- object
  - `approved` boolean, required
  - `client_name` string, nullable, required

## Other responses

- `400`: `too_many_attempts` — five failed entries by this principal
- `401`: Missing or invalid session JWT
- `403`: `insufficient_scope` — consenting beyond the consenter's role
- `404`: `not_found` — unknown, expired, already decided, or locked
- `422`: `invalid_request` — malformed body, bad code, or no workspace

---

[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)
