Executor version
1.6.10
How do you run Executor?
Self-host (Docker)
Operating system
Linux
Integration involved
GraphQL API (Shopify Admin API), OAuth client with grant client_credentials
What happened
I registered an OAuth client with grant: "client_credentials" and tokenEndpointAuthMethod: "body" for a Shopify Dev Dashboard app, then called POST /api/oauth/start to create the connection.
- Token URL:
https://{shop}.myshopify.com/admin/oauth/access_token
- Shopify accepts the request and returns HTTP 200 with a valid token.
- Executor rejects the response and returns HTTP 400:
OAuth client-credentials exchange failed: OAuth token exchange failed:
"response" body "token_type" property must be a string
(HTTP 200; body: {"access_token":"[redacted]","scope":"[redacted]","expires_in":86399})
Shopify's client credentials response has three fields: access_token, scope and expires_in. It never sends token_type. Shopify documents this shape here: https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/client-credentials-grant
The rejection comes from oauth.processClientCredentialsResponse in packages/core/sdk/src/oauth-helpers.ts (the clientCredentialsGrant path, around line 1396 on main). oauth4webapi requires token_type to be a string. The response goes to it unchanged.
What you expected
The connection is created and Executor renews the 24-hour token on its own.
Shopify stopped allowing new admin-created custom apps on 1 January 2026, so client credentials is now the only way for a server-side tool to get an Admin API token for a store in the same organisation. Without this, Shopify Admin cannot sign in through Executor at all. The only workaround is an outside job that fetches a token and replaces the connection every day.
Suggested fix
The authorization-code path already normalises a provider's odd response before oauth4webapi sees it (normalizeSlackTokenEnvelope). The same idea works here: when a 2xx token response has an access_token and no token_type, fill in token_type: "Bearer" before calling processClientCredentialsResponse. The refresh path would need the same treatment.
Two related details in Shopify's response, in case they matter to the same fix:
scope is comma-separated, not space-separated. normalizedTokenScope may already cover this.
- There is no
refresh_token. Renewal means repeating the client credentials request, which I believe Executor already does for this grant.
Steps to reproduce
- Create an app in the Shopify Dev Dashboard, give it any Admin API scope and install it on a store in the same organisation.
POST /api/oauth/clients with grant: "client_credentials", tokenEndpointAuthMethod: "body", the app's client ID and secret, and the token URL above.
- Add a GraphQL integration for
https://{shop}.myshopify.com/admin/api/2026-07/graphql.json with an oauth2 template that uses header X-Shopify-Access-Token and an empty prefix.
POST /api/oauth/start with that client, integration and template.
- The call returns HTTP 400 with the message above.
I checked main at 1.6.10 and the open and closed issues. #1447 and #1217 are near this area but neither covers a missing token_type.
Executor version
1.6.10
How do you run Executor?
Self-host (Docker)
Operating system
Linux
Integration involved
GraphQL API (Shopify Admin API), OAuth client with grant
client_credentialsWhat happened
I registered an OAuth client with
grant: "client_credentials"andtokenEndpointAuthMethod: "body"for a Shopify Dev Dashboard app, then calledPOST /api/oauth/startto create the connection.https://{shop}.myshopify.com/admin/oauth/access_tokenShopify's client credentials response has three fields:
access_token,scopeandexpires_in. It never sendstoken_type. Shopify documents this shape here: https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/client-credentials-grantThe rejection comes from
oauth.processClientCredentialsResponseinpackages/core/sdk/src/oauth-helpers.ts(theclientCredentialsGrantpath, around line 1396 onmain). oauth4webapi requirestoken_typeto be a string. The response goes to it unchanged.What you expected
The connection is created and Executor renews the 24-hour token on its own.
Shopify stopped allowing new admin-created custom apps on 1 January 2026, so client credentials is now the only way for a server-side tool to get an Admin API token for a store in the same organisation. Without this, Shopify Admin cannot sign in through Executor at all. The only workaround is an outside job that fetches a token and replaces the connection every day.
Suggested fix
The authorization-code path already normalises a provider's odd response before oauth4webapi sees it (
normalizeSlackTokenEnvelope). The same idea works here: when a 2xx token response has anaccess_tokenand notoken_type, fill intoken_type: "Bearer"before callingprocessClientCredentialsResponse. The refresh path would need the same treatment.Two related details in Shopify's response, in case they matter to the same fix:
scopeis comma-separated, not space-separated.normalizedTokenScopemay already cover this.refresh_token. Renewal means repeating the client credentials request, which I believe Executor already does for this grant.Steps to reproduce
POST /api/oauth/clientswithgrant: "client_credentials",tokenEndpointAuthMethod: "body", the app's client ID and secret, and the token URL above.https://{shop}.myshopify.com/admin/api/2026-07/graphql.jsonwith anoauth2template that uses headerX-Shopify-Access-Tokenand an empty prefix.POST /api/oauth/startwith that client, integration and template.I checked
mainat 1.6.10 and the open and closed issues. #1447 and #1217 are near this area but neither covers a missingtoken_type.