{"record":{"id":"d7910cc9a2f54bbf","repo":"coreyhaines31/marketingskills","slug":"no-jwt-in-response","errorCode":null,"errorMessage":"No JWT in response","messagePattern":"No JWT in response","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/clis/zoominfo.js","lineNumber":30,"sourceCode":"async function authenticate() {\n  if (ACCESS_TOKEN) return ACCESS_TOKEN\n  const username = process.env.ZOOMINFO_USERNAME\n  const password = process.env.ZOOMINFO_PRIVATE_KEY\n  if (!username || !password) {\n    throw new Error('ZOOMINFO_USERNAME and ZOOMINFO_PRIVATE_KEY required for authentication')\n  }\n  const res = await fetch(`${BASE_URL}/authenticate`, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ username, password }),\n  })\n  const text = await res.text()\n  if (!res.ok) {\n    throw new Error(`Authentication failed (${res.status}): ${text}`)\n  }\n  try {\n    const data = JSON.parse(text)\n    if (!data.jwt) throw new Error('No JWT in response')\n    ACCESS_TOKEN = data.jwt\n    return ACCESS_TOKEN\n  } catch (e) {\n    if (e.message === 'No JWT in response') throw e\n    throw new Error(`Authentication failed: ${text}`)\n  }\n}\n\nasync function api(method, path, body) {\n  if (args['dry-run']) {\n    return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ***' }, body }\n  }\n  const token = await authenticate()\n  const res = await fetch(`${BASE_URL}${path}`, {\n    method,\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${token}`,","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/coreyhaines31/marketingskills/blob/7868cb9251fad80a73d26e488a5ad5f6c4a9f335/tools/clis/zoominfo.js#L12-L48","documentation":"The /authenticate call returned a 2xx status and the body parsed as valid JSON, but the expected jwt field is absent or falsy. ZoomInfo's success contract is { jwt: \"...\" }; reaching this throw means the response was success-shaped but did not conform -- typically API/version drift, a maintenance page with 200 status, or an account lacking JWT issuance permission.","triggerScenarios":"A 2xx response whose JSON lacks a jwt key: ZoomInfo API version change renaming the token field, BASE_URL pointed at a tenant/region that returns a different shape, a partial response during maintenance, or an account plan without JWT access.","commonSituations":"BASE_URL (https://api.zoominfo.com) redirected or pointed at the wrong region, a recent ZoomInfo release that altered the authenticate response, or a proxy returning a 200 with a wrapped/extra-keyed body.","solutions":["Log the full parsed data object before throwing to see exactly what ZoomInfo returned.","Confirm BASE_URL matches the account's correct ZoomInfo region/endpoint.","Check ZoomInfo release notes or contact support for an authenticate-response schema change."],"exampleFix":"// before\nif (!data.jwt) throw new Error('No JWT in response')\n\n// after\nif (!data.jwt) throw new Error(`No JWT in response: ${JSON.stringify(data)}`)","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isZoominfoAuthResponse(v) {\n  return v != null && typeof v === 'object' && typeof v.jwt === 'string' && v.jwt.length > 0\n}","tryCatchPattern":"const data = JSON.parse(text)\nif (!isZoominfoAuthResponse(data)) {\n  throw new Error(`Unexpected ZoomInfo auth response shape: ${JSON.stringify(data)}`)\n}","preventionTips":["Pin to a known BASE_URL for your ZoomInfo tenant rather than a guessable host.","Log the full response body once during integration to lock in the expected jwt contract.","Subscribe to ZoomInfo API changelog notices to catch token-field renames early."],"tags":["authentication","api-contract","zoominfo"],"backgroundTag":null,"analyzedSha":"7868cb9251fad80a73d26e488a5ad5f6c4a9f335","analyzedAt":"2026-08-13T03:33:17.303Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}