PKCE code_verifier mismatch returning 400 Bad Request in Genesys Cloud OAuth flow

Looking for advice on implementing the Authorization Code flow with PKCE for a single-page application interacting with the Genesys Cloud API. I am generating a code_challenge using SHA-256 and Base64-URL encoding in JavaScript, but the token exchange endpoint /oauth/token consistently returns a 400 Bad Request with error: "invalid_grant" and the message Code verification failed.

Here is the relevant snippet for generating the challenge: const challenge = btoa(crypto.getRandomValues(new Uint8Array(32))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');. I am sending this value in the initial authorization request to https://api.mypurecloud.com/oauth/authorize. The response includes a valid code, but when I POST to the token endpoint with grant_type=authorization_code, code_verifier set to the original random string, and redirect_uri matching the config, the server rejects it.

I have verified that the client_id is correct and the redirect URI is registered in the developer portal. The code expires quickly, so I am making the token request immediately after receiving the callback. Is there a specific encoding requirement for the code_verifier that differs from the standard PKCE spec, or am I missing a parameter in the token request body?

To fix this easily, this is to ensure you are using base64url encoding without padding. the docs say “the code challenge must be base64url encoded” but most js libraries add padding which breaks it. try this:

function base64urlEncode(arrayBuffer) {
 const bytes = new Uint8Array(arrayBuffer);
 let binary = '';
 for (let i = 0; i < bytes.byteLength; i++) {
 binary += String.fromCharCode(bytes[i]);
 }
 return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

Ah, yeah, this is a known issue…

The suggestion above regarding base64url encoding is correct, but it is only half the battle. The SHA-256 hashing step is where most developers stumble, especially when moving between Node.js crypto modules and browser-based Web Crypto APIs. The code_verifier must be a cryptographically random string, and the code_challenge must be the base64url-encoded SHA-256 hash of that verifier. If you are using the standard btoa function in the browser, you must manually strip the padding (=) and replace + with - and / with _.

Here is a robust implementation for the browser environment that avoids common pitfalls:

async function generatePKCE() {
 // 1. Generate code_verifier (43-128 chars)
 const array = new Uint8Array(32);
 crypto.getRandomValues(array);
 const codeVerifier = btoa(String.fromCharCode(...array))
 .replace(/\+/g, '-')
 .replace(/\//g, '_')
 .replace(/=+$/, '');

 // 2. Generate code_challenge (SHA-256 of verifier)
 const encoder = new TextEncoder();
 const data = encoder.encode(codeVerifier);
 const hashBuffer = await crypto.subtle.digest('SHA-256', data);
 
 let codeChallenge = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)))
 .replace(/\+/g, '-')
 .replace(/\//g, '_')
 .replace(/=+$/, '');

 return { codeVerifier, codeChallenge };
}

When you call /oauth/authorize, pass code_challenge and code_challenge_method=S256. Then, in your token exchange request to /oauth/token, you must include the original code_verifier in the POST body. If the hash of the submitted code_verifier does not match the code_challenge from the authorization step, Genesys Cloud returns the invalid_grant error.

Also verify your OAuth scope. If you are using Client Credentials for a backend service, PKCE is irrelevant. PKCE is strictly for public clients (SPA/mobile) using Authorization Code flow. Ensure your client ID is configured for public access in the Genesys Cloud admin console under Integrations.

It depends, but generally… the padding issue is real. I hit this while tracing OAuth flows in my Datadog pipeline. The Web Crypto API returns raw bytes, so you must strip = manually after btoa. Check the spec here: https://developer.genesys.cloud/api/rest/authorization/oauth2/pkce.