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

  1. 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)"`
  2. 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).
  3. 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

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


AI-assisted analysis of coreyhaines31/marketingskills@7868cb9251 (2026-08-13). Data as JSON: /api/errors/b8751c8356cec6e7. Report an issue: GitHub.