PKCE Authorization Code Flow: 400 Bad Request on /oauth/token exchange

Implementing the Auth Code flow with PKCE for a SPA. The initial authorization request returns the code correctly, but exchanging it for a token via POST /oauth/token fails with a 400. The request payload includes the code, code_verifier, grant_type, and redirect_uri. Here’s the fetch call:

const res = await fetch('https://api.mypurecloud.com/oauth/token', {
 method: 'POST',
 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 body: new URLSearchParams({
 code: authCode,
 code_verifier: verifier,
 grant_type: 'authorization_code',
 redirect_uri: REDIRECT_URI
 })
});

Getting invalid_request in the response. The verifier matches the challenge hash. What am I missing?