## TL;DR

PKCE exists for public clients that cannot keep a secret. A backend service with a client secret should use the `client_credentials` grant instead. When the token endpoint rejects the PKCE exchange, switch the generated client to a direct client-credentials POST, cache the access token until expiry, and delete the redirect and code-verifier machinery entirely.

## The query

```text
agent implemented OAuth PKCE for a machine-to-machine integration - the provider only supports the client_credentials grant
```

## Use this when

- A generated integration implements PKCE for a backend-to-backend client.
- The token endpoint returns `unsupported_grant_type` or `invalid_grant` on the code exchange.
- The provider's docs list `client_credentials` as the machine-to-machine grant.

## Not for

- User-facing login flows (those genuinely need the authorization-code flow, with or without PKCE).
- Refresh-token wiring for user sessions.
- Providers that only support PKCE and have no client-credentials grant.

## Steps

### Step 1: Read the token error for the grant mismatch

```bash
curl -X POST "[token endpoint]" -d "grant_type=authorization_code" -d "code=[code]" | head -5
```

Expected output: an error naming the grant as unsupported. That confirms the flow is wrong, not the credentials. Stop debugging the code verifier. It was never going to work.

### Step 2: Confirm the provider's machine-to-machine grant

```bash
grep -i "client_credentials\|machine to machine\|M2M" provider-docs.md | head -10
```

Expected output: the docs name `client_credentials` as the grant for service clients. Note exactly how the provider wants the client id and secret sent (request body vs Basic header), because providers differ.

### Step 3: Rewrite the token fetch as client-credentials

```python
# machine-to-machine token fetch: no redirect, no code, no verifier
resp = requests.post("[token endpoint]", data={
    "grant_type": "client_credentials",
    "client_id": "[client id]",
    "client_secret": "[client secret]",
    "scope": "[scope]",
})
resp.raise_for_status()
```

Expected output: a 200 response with an `access_token` and `expires_in`. If the provider wants the credentials in a Basic header instead of the body, move them there per step 2's finding.

### Step 4: Cache the token and refresh on expiry

```python
# client_credentials has no refresh token -  re-run the same grant when expired
# client_credentials has no refresh token -  re-run the same grant when the cached credential nears expiry
creds = fetch_client_credentials_token()
```

Expected output: the client reuses the token until near expiry, then silently re-fetches. No refresh-token logic exists for this grant, so do not generate any.

### Step 5: Delete the PKCE machinery

```bash
grep -rn "code_verifier\|redirect_uri\|authorization_code" generated_client/ | head -10
```

Expected output: the list of PKCE remnants to remove (verifier generation, redirect handler, code-exchange call). A machine-to-machine client has no browser, no redirect, and no user, so every line referencing them is dead code that will confuse the next reader.

### Step 6: Add grant selection to the scaffolder's auth checklist

Add to the scaffolder checklist: machine-to-machine implies client_credentials unless the docs say otherwise.

Expected output: the next generated integration picks the grant from the client type (backend service vs user-facing app) instead of defaulting to the most documented flow.

## Variant phrasings

### scaffolder generated OAuth code against the wrong flow
Same root cause: the docs described auth-code first and the agent copied it. Steps 2 and 6 fix the selection.

### docs described auth-code flow but the API only supports client-credentials
Read past the quickstart. The machine-to-machine section is usually pages later, and it is the one that matters for service clients.

### agent picked implicit grant and the provider disabled it years ago
Same family of wrong-flow bug. Step 2's doc check catches it before code is generated.

## Why it happens

Scaffolders default to the most documented flow, and provider docs describe user login first because that is what most readers need. PKCE looks like the "modern, correct" choice to an agent pattern-matching on recency. But PKCE solves a problem machine-to-machine clients do not have (no safe place to keep a secret), while introducing machinery (redirects, verifiers, browser steps) that cannot work without a user. The grant must follow the client type, not the docs' page order.

## Edge cases

- Some providers require a `scope` or `audience` parameter on the client-credentials call. Omitting it yields a token that 403s on every API call.
- A few providers issue refresh tokens with client-credentials anyway. Do not rely on it. Re-running the grant is the portable behavior.
- If the provider rotates client secrets, the generated client needs a secret-reload path, not a hardcoded value. Read it from the environment at token-fetch time.
- Never commit the client secret alongside the generated code. The token fetch must read it from the environment, and the example config must ship with a placeholder.
- When the same provider serves both user-facing and M2M clients, generate two auth paths and select by configuration. One flow cannot serve both.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_Db0iIFj_oxrNE2jJrduOmg
