---
title: "Authorize Complete"
method: POST
path: "/v1/auth/oauth/authorize-complete"
tags: ["auth-oauth"]
---

# Authorize Complete

`POST /v1/auth/oauth/authorize-complete`

## Request

- Base URL: `https://api.usekeel.io` (the document declares no server; this is the origin it was published from)
- URL: `https://api.usekeel.io/v1/auth/oauth/authorize-complete`
- Auth: none declared

User finished the /connect page — mint a short-lived code and
return the redirect URL.

Auth: the user must be signed in (Clerk session cookie validated by
the existing get_principal dependency). PKCE parameters and the
redirect_uri come straight from the original client request via the
keel-app page; we re-validate redirect_uri server-side.

Redirect validation, two mutually exclusive paths:
* CIMD client (client_id is an HTTPS URL): fetch the metadata
  document; its redirect_uris are AUTHORITATIVE for this client.
* otherwise: the hardcoded static allow-list.

The response redirect carries RFC 9207 ``iss`` (always) and honours
an RFC 8707 ``resource`` indicator when the client sent one.

## Headers

- `Authorization` string, nullable
- `X-Org-Id` string, nullable

## Request body

- AuthorizeCompleteRequest: Body of /v1/auth/oauth/authorize-complete. Called by the /connect page after the user clicks Continue. The Clerk session cookie is the auth surface (verified by the standard keel-api middleware). The page passes the OAuth-flow parameters it received from the client through the URL.
  - `redirect_uri` string, required: Where to redirect the browser with the issued code.
  - `code_challenge` string, required: PKCE S256 code_challenge (base64url-encoded SHA-256 digest).
  - `code_challenge_method` 'S256': Only S256 is supported (plain is forbidden by OAuth 2.1).
  - `state` string, nullable: Opaque CSRF-protection value echoed back to the client.
  - `include_live` boolean: True if the user checked 'Include live trading'. Adds `runner.*` to the granted scopes — but ONLY when `requested_scope` permits the live tier (see the field below). The review-aware consent page (D6) never shows the checkbox for a base-only request; this server-side gate is the fail-safe that holds even if `include_live=on` is submitted anyway.
  - `requested_scope` string, nullable: The OAuth `scope` request parameter the client sent to the authorization endpoint (space-delimited), forwarded by the /connect page. A value that is present and omits the `live` tier (the listed/directory connector requests `scope=base`) forbids minting `runner.*` regardless of `include_live`. Absent → no narrowing (full/custom-connector + CLI keep the checkbox-driven behavior).
  - `include_execution_analytics_read` boolean: True only when the user explicitly consented to detailed execution-analytics reads. The exact `execution_analytics.read` token must also be present in `requested_scope`; this flag alone never widens access.
  - `include_execution_analytics_export` boolean: True only when the user explicitly consented to execution activity exports. The exact `execution_analytics.export` token must also be present in `requested_scope`; this flag alone never widens access.
  - `client_name` string, nullable: Display label for this connection (e.g. 'Claude Code'). Stored on the refresh-token row so the user can revoke individual clients from the settings page. Not used for auth or scope decisions — security comes from redirect_uri.
  - `client_id` string, nullable: OAuth client identifier. When it is an HTTPS URL it is treated as a Client ID Metadata Document (CIMD) per the current MCP auth spec: the server fetches the document and its `redirect_uris` become authoritative for this client (replacing the static allow-list). Non-URL client_ids are informational only.
  - `resource` string, nullable: RFC 8707 resource indicator — the canonical URL of the MCP server the token is being requested for, forwarded by the /connect page from the client's authorize request. Must exactly equal a hosted endpoint this authorization server issues tokens for (`invalid_target` otherwise). Absent → unchanged behaviour and no `aud` on the minted token.
  - `connect_attempt_id` string, nullable: The platform.connect_attempts id the /connect page carried (the `ca` URL parameter, Q-2381). Analytics only: links the attempt to this principal and is stored on the code so the token exchange can stamp it. A malformed value is DROPPED, never rejected, so measurement can never fail a consent.
  - `prompt_copied` 'clipboard' | 'select_fallback' | 'skipped', nullable: How the consent card's first prompt left the page (Q-2495): `clipboard` (the clipboard write resolved), `select_fallback` (the clipboard refused; the prompt was selected and the next click continued) or `skipped` (Return without copying). Absent for a card with no prompt (the Keel CLI). Measurement only: an unknown value is DROPPED, never rejected, so it can never fail a consent.
  - `device_class` 'desktop' | 'ios' | 'android' | 'unknown', nullable: The consenting device's class (Q-2584), reduced by keel-app from the request UA AND the card's input hint (a coarse pointer). It only REVISES an attempt the server render recorded as `desktop` from the UA alone -- Chrome on Android's desktop-site mode sends a desktop UA. Measurement only: an unknown value is DROPPED, never rejected.

## Response `200`

Successful Response

- AuthorizeCompleteResponse: Returns the URL the browser should redirect to.
  - `redirect_to` string, required: The full redirect_uri with `code` and `state` query parameters appended. The /connect page issues a 302 to this.

## Other responses

- `422`: Validation Error

---

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