Authorization Code flow with PKCE returns invalid_grant in SPA

Trying to wire up the Authorization Code flow with PKCE for a React app. We generate the code_verifier and code_challenge correctly, but the token endpoint keeps rejecting the exchange with invalid_grant. The redirect URI matches the config exactly.

Here is the POST payload to /api/v2/oauth/token. The code expires quickly, but we catch it immediately. Is the grant type wrong for this flow?

{
 "grant_type": "authorization_code",
 "code": "auth_code_xyz",
 "code_verifier": "verifier_abc",
 "redirect_uri": "https://app.example.com/callback"
}