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

# OAuth 2.0 Device Authorization Flow

> How headless agents authenticate with Mnemom using the OAuth 2.0 device-code grant (RFC 8628) — device-authorization request, user verification, token poll loop, and error handling.

Headless agents — CI runners, containerised workers, server-side pipelines — cannot open a browser to complete the authorization-code + PKCE flow. The **OAuth 2.0 device-authorization grant** ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)) is the correct mechanism for these callers: the agent requests a short `user_code`, the human operator approves it in a browser, and the agent polls the token endpoint until it receives an access token. This is exactly what `mnemom login --no-browser` runs under the hood for a headless CLI session — see [CLI Reference](/gateway/cli#authentication). For a full comparison of all supported authentication methods, see [Authentication](/guides/authentication).

## Flow overview

<Steps>
  <Step title="Request device and user codes">
    The agent sends `POST /v1/oauth/device_authorization` and receives a `device_code`, `user_code`, `verification_uri`, `expires_in`, and `interval`.
  </Step>

  <Step title="Display the code to the operator">
    The agent displays the `user_code` and `verification_uri` to the human operator — log them to a console, embed in a CLI prompt, or generate a QR code from `verification_uri_complete`.
  </Step>

  <Step title="Operator approves in a browser">
    The operator opens `verification_uri`, enters the `user_code`, and approves access. No action is required from the agent during this step.
  </Step>

  <Step title="Agent polls for the access token">
    The agent polls `POST /v1/oauth/token` with `grant_type=urn:ietf:params:oauth:grant-type:device_code` at the prescribed interval until it receives an access token or a terminal error.
  </Step>
</Steps>

***

## Device-authorization request

```http theme={null}
POST /v1/oauth/device_authorization HTTP/1.1
Host: api.mnemom.ai
Content-Type: application/x-www-form-urlencoded

client_id=<your_client_id>&scope=mcp:read%20mcp:write
```

Successful response (HTTP 200):

```json theme={null}
{
  "device_code": "GmRh...opaque",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://api.mnemom.ai/v1/oauth/device",
  "verification_uri_complete": "https://api.mnemom.ai/v1/oauth/device?user_code=WDJB-MJHT",
  "expires_in": 900,
  "interval": 5
}
```

| Field | Type | Description |
| - | - | - |
| `device_code` | string | Opaque code the agent presents when polling the token endpoint. Keep this secret. |
| `user_code` | string | Short, human-typeable code the operator enters at `verification_uri`. Display this to your operator. |
| `verification_uri` | string | URL the operator must visit to approve the request. |
| `verification_uri_complete` | string | `verification_uri` with `user_code` pre-filled as a query parameter — use this for QR codes or one-click links where possible. |
| `expires_in` | integer | Seconds until both codes expire. The agent must complete the poll loop before this window closes. |
| `interval` | integer | Minimum seconds the agent must wait between poll attempts. Do not poll faster than this; the server enforces the limit. |

***

## Poll loop

After the device-authorization request succeeds, poll `POST /v1/oauth/token` at `interval`-second intervals. On each `slow_down` response, permanently increase the interval by 5 seconds for the remainder of the session. Stop when you receive an access token or a terminal error.

<CodeGroup>
  ```python Python theme={null}
  import time
  import httpx

  CLIENT_ID = "<your_client_id>"

  def poll_for_token(device_code: str, interval: int, expires_in: int) -> dict:
      deadline = time.time() + expires_in
      while time.time() < deadline:
          time.sleep(interval)
          resp = httpx.post(
              "https://api.mnemom.ai/v1/oauth/token",
              data={
                  "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
                  "device_code": device_code,
                  "client_id": CLIENT_ID,
              },
          )
          body = resp.json()
          if resp.status_code == 200:
              return body  # access_token, token_type, expires_in, [refresh_token]
          error = body.get("error")
          if error == "authorization_pending":
              continue
          elif error == "slow_down":
              interval += 5  # back off permanently for this session
              continue
          elif error in ("expired_token", "access_denied"):
              raise RuntimeError(f"Device authorization failed: {error}")
          else:
              raise RuntimeError(f"Unexpected error: {error}")
      raise TimeoutError("Device code expired before authorization was granted")
  ```

  ```typescript TypeScript theme={null}
  const CLIENT_ID = "<your_client_id>";

  async function pollForToken(
    deviceCode: string,
    interval: number,
    expiresIn: number,
  ) {
    const deadline = Date.now() + expiresIn * 1000;
    let pollInterval = interval;
    while (Date.now() < deadline) {
      await new Promise((r) => setTimeout(r, pollInterval * 1000));
      const resp = await fetch("https://api.mnemom.ai/v1/oauth/token", {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "urn:ietf:params:oauth:grant-type:device_code",
          device_code: deviceCode,
          client_id: CLIENT_ID,
        }),
      });
      const body = await resp.json();
      if (resp.ok) return body; // { access_token, token_type, expires_in, refresh_token? }
      const { error } = body as { error: string };
      if (error === "authorization_pending") continue;
      if (error === "slow_down") { pollInterval += 5; continue; }
      throw new Error(`Device authorization failed: ${error}`);
    }
    throw new Error("Device code expired before authorization was granted");
  }
  ```
</CodeGroup>

***

## Error codes

The following error codes may be returned by `POST /v1/oauth/token` during the poll loop:

| `error` value | HTTP status | Meaning | Action |
| - | - | - | - |
| `authorization_pending` | 400 | The operator has not yet approved the request. | Continue polling after `interval` seconds. |
| `slow_down` | 400 | The agent is polling too fast. | Increase `interval` by 5 seconds permanently and continue polling. |
| `expired_token` | 400 | The `device_code` has expired (`expires_in` elapsed). | Start over: request a new `device_code` and prompt the operator again. |
| `access_denied` | 400 | The operator denied the request or cancelled it. | Abort. Do not retry with the same `device_code`. |
| `invalid_client` | 401 | The `client_id` is unknown or invalid. | Check your client registration. |

<Warning>
  Never poll faster than the current `interval` value. Each `slow_down` error permanently increases the required interval for that session. Repeated fast polling may result in the session being terminated with `access_denied`.
</Warning>

***

## Token response

A successful poll returns HTTP 200 with a token response:

```json theme={null}
{
  "access_token": "mcp_at_…",
  "token_type": "Bearer",
  "expires_in": 28800,
  "refresh_token": "mcp_rt_…",
  "scope": "mcp:read mcp:write"
}
```

Use the access token as a Bearer token on subsequent API requests:

```http theme={null}
Authorization: Bearer <access_token>
```

The refresh token can be exchanged for a new access token via the standard `refresh_token` grant at `POST /v1/oauth/token` before the access token expires.

***

## TTLs at a glance

| Item | TTL |
| - | - |
| `device_code` / `user_code` | `expires_in` seconds from the device-authorization response (typically 900 s / 15 min) |
| Access token | 28800 s (8 hours); the outgoing access token runs to its own 8-hour expiry after a refresh — it is not cut short. |
| Refresh token | 30 days sliding; single-use — each exchange issues a new refresh token. The outgoing refresh token has a **60 s grace window** after rotation before it is invalidated, to protect against network races. |

***

## Client registration

<Note>
  If you do not yet have a `client_id`, register your OAuth client with `POST /v1/oauth/register` (RFC 7591 dynamic client registration) before starting the device flow. Public clients (no client secret) are supported. See the [OAuth register endpoint](/api-reference/endpoint/post-oauth-register).
</Note>

<Warning>
  A self-registered device-only client (one registered with no `redirect_uris`) completes the flow, but the access token it receives carries no capability until Mnemom vets its `client_id`: the token response's `scope` comes back empty, API calls with it are refused as unauthenticated, and the consent screen tells the approving user so. `mnemom login --no-browser` uses Mnemom's own vetted client and is not affected. To use your own device-flow client, contact [support](mailto:support@mnemom.ai) to have its `client_id` reviewed.
</Warning>

***

## See also

* [Authentication](/guides/authentication) — full auth method comparison (passkeys, MFA, SSO, API keys)
* [API Keys](/guides/api-keys) — server-to-server auth without OAuth; simpler for non-delegated agent calls
* [API reference overview](/api-reference/overview) — auth header formats and rate limits


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.