{"record":{"id":"78bd6a4b93d21f4a","repo":"coreyhaines31/marketingskills","slug":"failed-to-obtain-access-token-78bd6a","errorCode":null,"errorMessage":"Failed to obtain access token","messagePattern":"Failed to obtain access token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/clis/snov.js","lineNumber":23,"sourceCode":"const BASE_URL = 'https://api.snov.io/v1'\n\nif (!CLIENT_ID || !CLIENT_SECRET) {\n  console.error(JSON.stringify({ error: 'SNOV_CLIENT_ID and SNOV_CLIENT_SECRET environment variables required' }))\n  process.exit(1)\n}\n\nlet cachedToken = null\n\nasync function getToken() {\n  if (cachedToken) return cachedToken\n  const res = await fetch('https://api.snov.io/v1/oauth/access_token', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ grant_type: 'client_credentials', client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),\n  })\n  const data = await res.json()\n  if (!data.access_token) {\n    throw new Error(data.error_description || data.error || 'Failed to obtain access token')\n  }\n  cachedToken = data.access_token\n  return cachedToken\n}\n\nasync function api(method, path, body) {\n  if (args['dry-run']) {\n    return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json' }, body: body || undefined }\n  }\n  const token = await getToken()\n  const opts = {\n    method,\n    headers: {\n      'Authorization': `Bearer ${token}`,\n      'Content-Type': 'application/json',\n      'Accept': 'application/json',\n    },\n  }","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/coreyhaines31/marketingskills/blob/7868cb9251fad80a73d26e488a5ad5f6c4a9f335/tools/clis/snov.js#L5-L41","documentation":"Snov.io access-token endpoint: the script POSTs grant_type=client_credentials to https://api.snov.io/v1/oauth/access_token and expects an access_token. This is the fallback message, surfaced only when the parsed JSON has no access_token and Snov also omitted error/error_description. Like the hotjar tool, the code parses res.json() unconditionally without checking res.ok.","triggerScenarios":"POST to /oauth/access_token returns JSON without access_token: wrong/revoked SNOV_CLIENT_ID or SNOV_CLIENT_SECRET, a deactivated Snov account, a rate-limited token endpoint returning a non-standard body, or any non-2xx status (no res.ok gate).","commonSituations":"SNOV_CLIENT_ID/SECRET not loaded because the script only reads process.env and there is no dotenv loader, free-tier quotas blocking token issuance, credentials rotated in the Snov UI but not updated locally, or env vars set under a different name.","solutions":["Confirm the env vars are set in the executing shell: `node -e \"console.log(!!process.env.SNOV_CLIENT_ID, !!process.env.SNOV_CLIENT_SECRET)\"`","Log res.status and the full data object before throwing to expose Snov's actual error reason.","Regenerate the API credentials in Snov -> Settings -> API users and re-export SNOV_CLIENT_ID and SNOV_CLIENT_SECRET."],"exampleFix":"// before\nconst data = await res.json()\nif (!data.access_token) {\n  throw new Error(data.error_description || data.error || 'Failed to obtain access token')\n}\n\n// after\nconst data = await res.json()\nif (!res.ok || !data.access_token) {\n  throw new Error(`Snov token request failed (${res.status}): ${JSON.stringify(data)}`)\n}","handlingStrategy":"try-catch","validationCode":"function validateSnovCreds() {\n  const missing = ['SNOV_CLIENT_ID', 'SNOV_CLIENT_SECRET'].filter(k => !process.env[k])\n  if (missing.length) throw new Error(`Missing env vars: ${missing.join(', ')}`)\n}","typeGuard":"function hasAccessToken(v) {\n  return v != null && typeof v.access_token === 'string' && v.access_token.length > 0\n}","tryCatchPattern":"try {\n  await snovApi('GET', '/get-user-info')\n} catch (e) {\n  if (/access token|token request/i.test(e.message)) {\n    throw new Error(`Snov auth failed -- verify SNOV_CLIENT_ID/CLIENT_SECRET: ${e.message}`)\n  }\n  throw e\n}","preventionTips":["Load .env before invoking (the tool has no built-in dotenv loader), e.g. `set -a; . ./.env; set +a`.","Confirm the Snov account is active and not throttled before long-running bulk operations.","Keep API credentials in a secrets manager rather than pasting them into shell history."],"tags":["oauth","authentication","snov","configuration"],"backgroundTag":null,"analyzedSha":"7868cb9251fad80a73d26e488a5ad5f6c4a9f335","analyzedAt":"2026-08-13T03:33:17.303Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}