coreyhaines31/marketingskills · error · Error
Failed to obtain access token
Error message
Failed to obtain access token
What it means
Hotjar's OAuth2 client-credentials flow: the script POSTs grant_type=client_credentials to https://api.hotjar.io/oauth/token and expects an access_token in the JSON response. This error is the FINAL fallback, raised only when the parsed body has no access_token AND no error_description/error fields to report. Because the code never checks res.ok before parsing, a non-2xx status with an atypical JSON body also lands here with an uninformative generic message.
Source
Thrown at tools/clis/hotjar.js:24
const BASE_URL = 'https://api.hotjar.io/v2'
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error(JSON.stringify({ error: 'HOTJAR_CLIENT_ID and HOTJAR_CLIENT_SECRET environment variables required' }))
process.exit(1)
}
let cachedToken = null
async function getToken() {
if (cachedToken) return cachedToken
const res = await fetch(`${OAUTH_URL}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=client_credentials&client_id=${encodeURIComponent(CLIENT_ID)}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`,
})
const data = await res.json()
if (!data.access_token) {
throw new Error(data.error_description || data.error || 'Failed to obtain access token')
}
cachedToken = data.access_token
return cachedToken
}
async function api(method, path) {
if (args['dry-run']) {
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json' } }
}
const token = await getToken()
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
})View on GitHub (pinned to 7868cb9251)
Solutions
- Confirm the env vars are present and non-empty in the executing shell: `node -e "console.log(!!process.env.HOTJAR_CLIENT_ID, !!process.env.HOTJAR_CLIENT_SECRET)"`
- Surface Hotjar's real response by checking res.ok and logging res.status plus the raw body before throwing (the generic message hides the cause).
- Re-issue the client credentials in the Hotjar admin and confirm the app has the required resources/scopes, then re-export HOTJAR_CLIENT_ID and HOTJAR_CLIENT_SECRET.
Example fix
// before
const data = await res.json()
if (!data.access_token) {
throw new Error(data.error_description || data.error || 'Failed to obtain access token')
}
// after
const data = await res.json()
if (!res.ok || !data.access_token) {
throw new Error(`Hotjar token request failed (${res.status}): ${JSON.stringify(data)}`)
} Defensive patterns
Strategy: try-catch
Validate before calling
function validateHotjarCreds() {
const missing = ['HOTJAR_CLIENT_ID', 'HOTJAR_CLIENT_SECRET'].filter(k => !process.env[k])
if (missing.length) throw new Error(`Missing env vars: ${missing.join(', ')}`)
} Type guard
function hasAccessToken(v) {
return v != null && typeof v.access_token === 'string' && v.access_token.length > 0
} Try / catch
try {
await hotjarApi('GET', '/sites')
} catch (e) {
if (/access token|token request/i.test(e.message)) {
throw new Error(`Hotjar auth failed -- verify HOTJAR_CLIENT_ID/CLIENT_SECRET: ${e.message}`)
}
throw e
} Prevention
- Run the CLI with `--dry-run` first to confirm env vars load before any network call.
- Source the .env file explicitly in the same shell that runs the tool (the script only reads process.env).
- Rotate Hotjar credentials in one place and update every environment the same day to avoid drift.
When it happens
Trigger: POST to /oauth/token returns JSON without access_token: invalid/expired/revoked HOTJAR_CLIENT_ID or HOTJAR_CLIENT_SECRET, the Hotjar app lacking the requested scope/resources, a Hotjar-side error whose body omits the standard error fields, or any non-2xx status (the script does not gate on res.ok).
Common situations: Credentials copied with trailing whitespace or surrounding quotes, env vars exported in a different shell than the one running the CLI, a stale .env after rotating Hotjar keys, an app still pending Hotjar approval, or the script invoked without sourcing the .env file (it only reads process.env).
Related errors
- Failed to obtain access token
- ZOOMINFO_USERNAME and ZOOMINFO_PRIVATE_KEY required for auth
- Authentication failed (${res.status}): ${text}
- Authentication failed: ${text}
- No JWT in response
AI-assisted analysis of coreyhaines31/marketingskills@7868cb9251 (2026-08-13).
Data as JSON: /api/errors/b8751c8356cec6e7.
Report an issue: GitHub.