> ## Documentation Index > Fetch the complete documentation index at: https://openrouter.ai/docs/llms.txt > Use this file to discover all available pages before exploring further. # OAuth PKCE > Connect your users to OpenRouter Users can connect to OpenRouter in one click using [Proof Key for Code Exchange (PKCE)](https://oauth.net/2/pkce/). Here's a step-by-step guide: ## PKCE Guide ### Step 1: Send your user to OpenRouter To start the PKCE flow, send your user to OpenRouter's `/auth` URL with a `callback_url` parameter pointing back to your site: ```txt title="With S256 Code Challenge (Recommended)" wrap lines theme={null} https://openrouter.ai/auth?callback_url=&code_challenge=&code_challenge_method=S256 ``` ```txt title="With Plain Code Challenge" wrap lines theme={null} https://openrouter.ai/auth?callback_url=&code_challenge=&code_challenge_method=plain ``` ```txt title="Without Code Challenge" wrap lines theme={null} https://openrouter.ai/auth?callback_url= ``` The `code_challenge` parameter is optional but recommended. Your user will be prompted to log in to OpenRouter and authorize your app. After authorization, they will be redirected back to your site with a `code` parameter in the URL: Alt text **Use SHA-256 for Maximum Security** For maximum security, set `code_challenge_method` to `S256`, and set `code_challenge` to the base64 encoding of the sha256 hash of `code_verifier`. For more info, [visit Auth0's docs](https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-authorization-code-flow-with-pkce#parameters). #### How to Generate a Code Challenge The following example uses the Web Crypto API and the Buffer API to generate a code challenge for the S256 method. You will need a bundler to use the Buffer API in the web browser: ```typescript title="Generate Code Challenge" lines theme={null} import { Buffer } from 'buffer'; async function createSHA256CodeChallenge(input: string) { const encoder = new TextEncoder(); const data = encoder.encode(input); const hash = await crypto.subtle.digest('SHA-256', data); return Buffer.from(hash).toString('base64url'); } const codeVerifier = 'your-random-string'; const generatedCodeChallenge = await createSHA256CodeChallenge(codeVerifier); ``` #### Localhost Apps Localhost callbacks are supported on **any port**. This is useful for CLI tools and local-first apps that bind to an arbitrary free OS port for the OAuth callback (e.g. `http://localhost:51423/callback`). Localhost apps are assigned a fixed title matching the host and port (e.g. `localhost:3000`) but will not appear in the OpenRouter marketplace or rankings. If you want a custom app name and marketplace presence, use a public URL as the callback instead. When moving to production, replace the localhost callback URL with a public URL (your project website or a GitHub repo link) to get full app attribution. #### Headless Apps (SSH Servers, Containers) If your app runs where a localhost callback can't be reached (an SSH session, a remote dev box, a container), omit `callback_url` entirely: ```txt title="Headless (No Callback)" wrap lines theme={null} https://openrouter.ai/auth?code_challenge=&code_challenge_method=S256&key_label= ``` After the user authorizes, the page displays the authorization code on screen instead of redirecting. The user copies it and pastes it into your app (e.g. at a terminal prompt), and you exchange it in Step 2 exactly as usual. A `code_challenge` is **required** in this mode: because the code is displayed on screen, PKCE ensures it is useless to anyone without your app's `code_verifier`. The code is single-use and expires after 10 minutes. ### Step 2: Exchange the code for a user-controlled API key After the user logs in with OpenRouter, they are redirected back to your site with a `code` parameter in the URL: Alt text Extract this code using the browser API: ```typescript title="Extract Code" lines theme={null} const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); ``` Then use it to make an API call to `https://openrouter.ai/api/v1/auth/keys` to exchange the code for a user-controlled API key: ```typescript title="Exchange Code" lines theme={null} const response = await fetch('https://openrouter.ai/api/v1/auth/keys', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ code: '', code_verifier: '', // If code_challenge was used code_challenge_method: '', // If code_challenge was used }), }); const { key } = await response.json(); ``` ### Deep-link to the user's key Once you have the API key, you can create links to the user's OpenRouter activity and key settings pages by hashing the key with SHA-256. Use the lowercase hexadecimal digest in both URLs: ```typescript title="Create Key Links" lines theme={null} async function sha256Hex(value: string) { const data = new TextEncoder().encode(value); const hash = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, '0'), ).join(''); } const keyHash = await sha256Hex(key); const logsUrl = `https://openrouter.ai/logs?api_key_hash=${keyHash}`; const settingsUrl = `https://openrouter.ai/keys/${keyHash}`; ``` The links only work for the signed-in owner of the API key. If the hash does not resolve for the viewer, the page returns a `404` rather than showing unfiltered data. And that's it for the PKCE flow! ### Step 3: Use the API key Store the API key securely within the user's browser or in your own database, and use it to [make OpenRouter requests](/docs/api_reference/overview). ```typescript title="TypeScript SDK" lines theme={null} import { OpenRouter } from '@openrouter/sdk'; const openRouter = new OpenRouter({ apiKey: key, // The key from Step 2 }); const completion = await openRouter.chat.send({ chatRequest: { model: '~openai/gpt-sol-latest', messages: [ { role: 'user', content: 'Hello!', }, ], stream: false, }, }); if (completion instanceof ReadableStream) { throw new Error('Expected a non-streaming response'); } console.log(completion.choices[0].message); ``` ```typescript title="TypeScript (fetch)" lines theme={null} fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: '~openai/gpt-sol-latest', messages: [ { role: 'user', content: 'Hello!', }, ], }), }); ``` ## Error Codes * `400 Invalid code_challenge_method`: Make sure you're using the same code challenge method in step 1 as in step 2. * `403 Invalid code or code_verifier`: Make sure your user is logged in to OpenRouter, and that `code_verifier` and `code_challenge_method` are correct. * `403 Authorization code expired`: Authorization codes expire 10 minutes after issuance. Restart the OAuth flow and exchange the new code promptly. * `405 Method Not Allowed`: Make sure you're using `POST` and `HTTPS` for your request. ## External Tools * [PKCE Tools](https://example-app.com/pkce) * [Online PKCE Generator](https://tonyxu-io.github.io/pkce-generator/)