coreyhaines31/marketingskills · error · Error

Failed to obtain access token

Error message

Failed to obtain access token

What it means

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.

Source

Thrown at tools/clis/snov.js:23

const BASE_URL = 'https://api.snov.io/v1'

if (!CLIENT_ID || !CLIENT_SECRET) {
  console.error(JSON.stringify({ error: 'SNOV_CLIENT_ID and SNOV_CLIENT_SECRET environment variables required' }))
  process.exit(1)
}

let cachedToken = null

async function getToken() {
  if (cachedToken) return cachedToken
  const res = await fetch('https://api.snov.io/v1/oauth/access_token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ grant_type: 'client_credentials', client_id: CLIENT_ID, client_secret: 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, body) {
  if (args['dry-run']) {
    return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json' }, body: body || undefined }
  }
  const token = await getToken()
  const opts = {
    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 set in the executing shell: `node -e "console.log(!!process.env.SNOV_CLIENT_ID, !!process.env.SNOV_CLIENT_SECRET)"`
  2. Log res.status and the full data object before throwing to expose Snov's actual error reason.
  3. Regenerate the API credentials in Snov -> Settings -> API users and re-export SNOV_CLIENT_ID and SNOV_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(`Snov token request failed (${res.status}): ${JSON.stringify(data)}`)
}
Defensive patterns

Strategy: try-catch

Validate before calling

function validateSnovCreds() {
  const missing = ['SNOV_CLIENT_ID', 'SNOV_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 snovApi('GET', '/get-user-info')
} catch (e) {
  if (/access token|token request/i.test(e.message)) {
    throw new Error(`Snov auth failed -- verify SNOV_CLIENT_ID/CLIENT_SECRET: ${e.message}`)
  }
  throw e
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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