OAuth authorization_code exchange fails with "Not Authorized" — reproducible across 2 apps, 3 test accounts, and production

## Summary

Every attempt to exchange a valid OAuth authorization_code for an access token via POST /oauth2/token fails with:

{ “message”: “Not Authorized”, “type”: “service.not_authorized” }

This happens 100% of the time, regardless of which application, which seller account, or which environment (Sandbox or Production) is used. I’ve isolated this specifically to the authorization_code grant type — directly using an already-issued access token, or using the Developer Console’s own “Renew token” feature, both work fine. So this doesn’t look like a general account restriction — it looks like something specifically blocking the OAuth code-exchange step for my developer account.

## Reproduction steps

1. Send user to /oauth2/authorize with valid client_id, scope, state.

2. User approves on the real Square consent screen (confirmed working correctly — correct app name, correct scopes shown).

3. Square redirects back with a valid, unused, unexpired code.

4. POST to /oauth2/token with client_id, client_secret, code, grant_type: authorization_code (JSON body, Content-Type: application/json) — fails every time with the error above.

## What I’ve ruled out

- Wrong/malformed secret: Regenerated the Sandbox Application secret 3 times from the correct location (OAuth tab, not Credentials tab), confirmed correct prefix (sandbox-sq0csb-), correct length, no whitespace/newlines each time.

- App-specific corruption: Reproduced on a completely brand-new application (sq0idp-c8q8A9cboJ1FxTYZ5pjpJg, “Shears and Beards Site”) created specifically to rule this out, in addition to the original app (sq0idp-wFcpcPpH6pGphYd46BsDQg, “Shears and Beards Booking”). Identical failure on both.

- Seller-account-specific issue: Reproduced with 3 different Sandbox test accounts (“Default Test Account”, “Shears & Beards (Shop)”, “Marcus Stand-in (Contractor)”). Identical failure with all three.

- Sandbox-only bug: Reproduced in Production as well, with my own real Square merchant account logging in and approving through the real consent screen. Identical “Not Authorized” error.

- Client-side code bug: Reproduced the exact same failure using Square’s own official API Explorer tool (Square API Explorer, Obtain Token endpoint) with the same client_id/client_secret/code — so this isn’t a bug in my server code.

- Minimal scope: Reproduced even when requesting just MERCHANT_PROFILE_READ alone (no Appointments/Items scopes), so it’s not a permissions/feature-gating issue on a specific scope.

## What does work (for contrast)

- Using the Developer Console’s “Renew token” button for an existing test-account token, then calling GET /v2/merchants with that token: returns 200 OK with real merchant data. So Sandbox itself, and token-based API auth in general, are healthy for my account.

- This confirms the failure is specific to the authorization_code to /oauth2/token exchange step itself, not a broader account or API-access problem.

## Apps/IDs involved

- Sandbox: sandbox-sq0idb-VAd1FszISeUaXu4E3Wj3uA (app: Shears and Beards Booking, sq0idp-wFcpcPpH6pGphYd46BsDQg)

- Sandbox: sandbox-sq0idb-1p5m6AjpcScGaz5FtcL17g (app: Shears and Beards Site, sq0idp-c8q8A9cboJ1FxTYZ5pjpJg)

- Production: sq0idp-c8q8A9cboJ1FxTYZ5pjpJg (same app, Production credentials)

## Ask

Could someone look into whether there’s an account-level restriction or flag blocking the authorization_code OAuth grant type specifically for my developer account? Happy to provide timestamps of specific failed requests if that helps you find them in logs.

Thanks!