Genesys Cloud SPA Auth Code Flow PKCE returning 400 Bad Request

Getting a 400 error when swapping the authorization code for tokens in my single-page app. I’m trying to implement PKCE properly because the docs say it’s required now. The initial redirect works fine, I get the code back in the URL. But the POST to /oauth/token fails immediately.

Here’s the payload I’m sending:

{
 "grant_type": "authorization_code",
 "code": "8xK9mP2vLq",
 "redirect_uri": "http://localhost:3000/callback",
 "client_id": "my-app-client-id",
 "code_verifier": "dGhpcyBpcyBhIHZlcnlfbG9uZ19zZWNyZXRfcmFuZG9tX3N0cmluZw"
}

I generated the code verifier using a SHA-256 hash of a random string, then base64url encoded it for the challenge. The verifier matches the challenge exactly. I’ve checked the redirect URI matches the one registered in the OAuth client settings. The error response body is empty, just the status code. I’m using standard fetch API. Any idea what I’m missing? Is the encoding wrong?