twentyhq/twenty · error · Error
Token exchange failed: ${response.status} ${response.statusT
Error message
Token exchange failed: ${response.status} ${response.statusText} What it means
Thrown by exchangeCredentialsForTokens when the OAuth token endpoint returns a non-2xx HTTP status. The function POSTs client_credentials grant type to {apiUrl}/oauth/token, and any non-ok response is treated as a token exchange failure. The HTTP status code and status text are included for diagnostics.
Source
Thrown at packages/twenty-sdk/src/cli/utilities/auth/exchange-credentials-for-tokens.ts:20
export const exchangeCredentialsForTokens = async (
configService: ConfigService,
params: { clientId: string; clientSecret: string },
): Promise<{ accessToken: string; refreshToken?: string }> => {
const config = await configService.getConfig();
const response = await fetch(`${config.apiUrl}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: params.clientId,
client_secret: params.clientSecret,
}),
});
if (!response.ok) {
throw new Error(
`Token exchange failed: ${response.status} ${response.statusText}`,
);
}
const data = (await response.json()) as {
access_token: string;
refresh_token?: string;
};
await configService.setConfig({
appAccessToken: data.access_token,
...(data.refresh_token ? { appRefreshToken: data.refresh_token } : {}),
});
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
};View on GitHub (pinned to 1f5dd2bbd2)
Solutions
- If status is 401: re-run the SDK registration command to get fresh clientId/clientSecret credentials.
- If status is 400: verify the apiUrl in config is correct and points to the right Twenty instance.
- If status is 500/502/503: the server is likely down or overloaded — retry after a brief wait.
- Check that the client_secret was copied completely (no truncation) and has no leading/trailing whitespace.
- Ensure the apiUrl uses the correct protocol and path (https://, no trailing slash issues).
Example fix
// before
const { accessToken } = await exchangeCredentialsForTokens(configService, {
clientId,
clientSecret,
});
// after — handle specific HTTP statuses
try {
const { accessToken } = await exchangeCredentialsForTokens(configService, {
clientId, clientSecret,
});
} catch (e) {
if (e.message.includes('401')) {
console.error('Invalid credentials. Re-run registration to get a new client secret.');
}
throw e;
} Defensive patterns
Strategy: try-catch
Try / catch
try {
const { accessToken } = await exchangeCredentialsForTokens(configService, { clientId, clientSecret });
} catch (error) {
if (error instanceof Error && error.message.startsWith('Token exchange failed')) {
const status = error.message.match(/HTTP (\d+)/)?.[1];
if (status === '401') {
console.error('Invalid client credentials. Re-run registration to obtain new secrets.');
} else if (status?.startsWith('5')) {
console.error('Server error. Retry in a moment.');
}
process.exit(1);
}
throw error;
} Prevention
- Store client_secret securely and completely — avoid truncation during copy.
- Re-run the registration command if credentials may have been rotated.
- Verify the apiUrl in config matches the environment (staging vs production).
When it happens
Trigger: The OAuth /oauth/token endpoint returns 401 (invalid client_id/client_secret), 400 (malformed request), 403 (forbidden), 500 (server error), or any other non-2xx status. This happens during the SDK CLI authentication flow when exchanging app registration credentials for access tokens.
Common situations: The client_secret is wrong, expired, or was regenerated since it was stored. The client_id doesn't match a valid app registration. The apiUrl is wrong (pointing to a different environment). The OAuth server is temporarily down. The client_credentials grant is not enabled for this app registration.
Related errors
- ${res.statusText}: ${await res.text()}
- ${response.statusText}: ${response.rawBody}
- Failed to create app registration: ${errorDetail}
- Failed to introspect core schema: ${JSON.stringify(coreSchem
- App connection ${connectionId} requires the user to reconnec
AI-assisted analysis of twentyhq/twenty@1f5dd2bbd2 (2026-08-12).
Data as JSON: /api/errors/ce8df653b24d4c78.
Report an issue: GitHub.