Sign in with Poe (OAuth)

Programmatically obtain an API key on behalf of a user to query Poe bots and models on their behalf.

The flow uses the OAuth 2.0 Authorization Code Grant with mandatory PKCE for security.

How it works

  1. Your app redirects the user to Poe's authorization page
  2. The user reviews the connection and clicks "Connect"
  3. Poe redirects back to your app with an authorization code
  4. Your app exchanges the code for an API key

Step 1: Register your client

Go to poe.com/api/clients and click "Create client".

FieldDescription
Client NameDisplay name shown to users on the consent screen
Redirect URIsOne or more callback URLs where Poe sends the authorization code after the user approves. localhost URIs do not need to be registered and work automatically for local development.

After creation, copy your "Client ID" — you will need this in a later step. You can edit or delete your client from this page at any time.

Step 2: Generate PKCE parameters

PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. Instead of a client secret, your app creates a one-time code verifier (a random string) and sends its SHA-256 hash — the code challenge — in the authorization request. When you exchange the code for an API key, you send the original verifier so Poe can confirm the same app that started the flow is finishing it.

const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
const codeVerifier = btoa(String.fromCharCode(...bytes))
  .replace(/\+/g, "-")
  .replace(/\//g, "_")
  .replace(/=+$/, "");

const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
  .replace(/\+/g, "-")
  .replace(/\//g, "_")
  .replace(/=+$/, "");

// Store codeVerifier — you'll need it in Step 5
import crypto from "node:crypto";

const codeVerifier = crypto.randomBytes(32).toString("base64url");

const codeChallenge = crypto
  .createHash("sha256")
  .update(codeVerifier)
  .digest("base64url");

// Store codeVerifier — you'll need it in Step 5
import hashlib
import base64
import secrets

code_verifier = secrets.token_urlsafe(32)

digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")

# Store code_verifier — you'll need it in Step 5

Step 3: Send your user to Poe

Build the authorization URL (https://poe.com/oauth/authorize) and redirect the user to it:

const params = new URLSearchParams({
  client_id: "<CLIENT_ID>",
  redirect_uri: "https://yourapp.com/callback",
  response_type: "code",
  scope: "apikey:create",
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
});

window.location.href = `https://poe.com/oauth/authorize?${params}`;
// Express
app.get("/auth/poe", (req, res) => {
  const params = new URLSearchParams({
    client_id: "<CLIENT_ID>",
    redirect_uri: "https://yourapp.com/callback",
    response_type: "code",
    scope: "apikey:create",
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
  });

  res.redirect(`https://poe.com/oauth/authorize?${params}`);
});
# FastAPI
from fastapi.responses import RedirectResponse
from urllib.parse import urlencode

@app.get("/auth/poe")
def login():
    params = urlencode({
        "client_id": "<CLIENT_ID>",
        "redirect_uri": "https://yourapp.com/callback",
        "response_type": "code",
        "scope": "apikey:create",
        "code_challenge": code_challenge,
        "code_challenge_method": "S256",
    })
    return RedirectResponse(f"https://poe.com/oauth/authorize?{params}")
ParameterRequiredDescription
client_idYesYour app's Client ID
redirect_uriYesMust exactly match one of your registered redirect URIs
response_typeYesMust be code
scopeYesMust be apikey:create
code_challengeYesBase64url-encoded SHA-256 hash of the code verifier
code_challenge_methodYesMust be S256
stateNoOpaque value returned unchanged in the redirect. Can be used to prevent CSRF

Step 4: User approves the connection

The user sees a consent screen and can optionally choose an API key expiration.

On success, Poe redirects to your redirect_uri with a code parameter. Extract it in your callback:

const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const error = params.get("error");
const errorDescription = params.get("error_description");

if (error) {
  // Handle error (see table below)
}
// Express
app.get("/callback", (req, res) => {
  const code = req.query.code;
  const error = req.query.error;
  const errorDescription = req.query.error_description;

  if (error) {
    // Handle error (see table below)
  }
});
# FastAPI
@app.get("/callback")
def callback(code: str = None, error: str = None, error_description: str = None):
    if error:
        # Handle error (see table below)
        pass

On error, Poe redirects with error and error_description:

errorCause
access_deniedThe user clicked "Deny"
invalid_requestMissing or malformed required parameters
unsupported_response_typeresponse_type is not code
invalid_scopeScope is missing or not supported

Step 5: Exchange code for API key

The authorization code is short-lived and can only be used once. Exchange it for an API key at https://api.poe.com/token.

const response = await fetch("https://api.poe.com/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    client_id: "<CLIENT_ID>",
    code,
    redirect_uri: "https://yourapp.com/callback",
    code_verifier: codeVerifier,
  }),
});

const { api_key, api_key_expires_in } = await response.json();
const response = await fetch("https://api.poe.com/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    client_id: "<CLIENT_ID>",
    code,
    redirect_uri: "https://yourapp.com/callback",
    code_verifier: codeVerifier,
  }),
});

const { api_key, api_key_expires_in } = await response.json();
import requests

response = requests.post(
    "https://api.poe.com/token",
    data={
        "grant_type": "authorization_code",
        "client_id": "<CLIENT_ID>",
        "code": code,
        "redirect_uri": "https://yourapp.com/callback",
        "code_verifier": code_verifier,
    },
    headers={"Content-Type": "application/x-www-form-urlencoded"},
)

data = response.json()
api_key = data["api_key"]
expires_in = data["api_key_expires_in"]  # seconds, or None if no expiry
curl -X POST https://api.poe.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "client_id=<CLIENT_ID>" \
  -d "code=<CODE>" \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "code_verifier=<CODE_VERIFIER>"
FieldTypeDescription
api_keystringThe API key to use for Poe API requests
api_key_expires_innumber or nullSeconds until the key expires, or null if it does not expire

On error, the response includes error and error_description:

errorCause
invalid_requestMissing parameter, invalid Content-Type, or too many requests
invalid_grantCode expired or invalid, PKCE verification failed, or parameter mismatch
unsupported_grant_typegrant_type is not authorization_code
server_errorInternal error — safe to retry

Users can view the key issued through your app at poe.com/api/keys.

Step 6: Use the API key

Use the API key to make requests to the Poe API on behalf of the user. See the OpenAI Compatible API for usage examples.

Support

Feel free to reach out to support if you have questions or run into unexpected behavior.