PKCE code_verifier mismatch on Genesys Cloud OAuth for SPA

Trying to wire up a simple SPA auth flow against the Genesys Cloud identity endpoint. We’re using the Authorization Code flow with PKCE. The initial redirect to /oauth2/authorize works fine, brings back the code. But the token exchange step is failing hard. I’m generating the code_verifier in JS before the redirect and sending it along with the code and code_challenge_method=S256 in the POST body to /oauth2/token. The response is a 400 Bad Request with invalid_grant and the message Code verification failed. I’ve checked the base64url encoding multiple times. Here’s the fetch call for the token:

fetch('https://api.mypurecloud.com/oauth2/token', {
 method: 'POST',
 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 body: new URLSearchParams({
 grant_type: 'authorization_code',
 code: queryCode,
 redirect_uri: window.location.origin,
 client_id: 'my-spa-client-id',
 code_verifier: storedVerifier
 })
})

The storedVerifier matches what was used to generate the challenge. I’m not using the SDK for this part, just raw fetch. The docs imply this should be straightforward. Is there a specific character set restriction I’m missing? Or maybe the encoding function in JS is dropping padding characters that the server expects? I’ve tried with and without padding. Nothing changes. The error is always the same. Running out of ideas here.