You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过服务账户、REST API与TypeScript认证并发送Google Ads点击转化

Google Ads服务账户认证问题(REST接口)

我未使用官方TypeScript/JavaScript客户端库,自行参照Google Ads的REST认证文档与点击转化上传接口,尝试实现服务账户认证并向Google Ads发送点击转化数据,但目前卡在认证环节。需先解决认证问题,若后续发送转化仍有问题也需一并处理。

当前遇到的错误截图:
错误截图

我的配置情况

  • 拥有带开发者令牌的Google Ads账户,该令牌将在发送点击转化时使用,与服务账户认证无关。
  • 已在Google Cloud项目中创建服务账户,并启用了Google Ads API:
    服务账户配置
  • 已将服务账户的client ID添加到工作区的全域授权,权限范围为https://www.googleapis.com/auth/adwords:
    全域授权配置
  • 上述操作在「APIs & Services」中生成了对应的「OAuth 2.0 Client IDs」,且client ID匹配:
    OAuth客户端ID
  • 额外配置了测试状态的外部OAuth同意屏幕,权限范围同样为https://www.googleapis.com/auth/adwords,但我认为服务账户认证无需此配置。
  • 服务账户本身无其他相关权限,文档未提及需要额外权限,我认为添加全域授权已足够,但不确定是否有误。

我的猜测

问题可能出在两个方面:

  • 权限不足;
  • 对authenticateToGoogleAdsManager函数中的refreshToken理解错误。我生成了签名JWT,但通过await fetch('https://oauth2.googleapis.com/token')仅能获取access token,不确定是否应将JWT作为refreshToken传入。

测试代码与函数实现

测试代码

// 生成签名JWT
const jwt = generateJsonWebTokenForServiceAccount(
  serviceAccount,
  ['https://www.googleapis.com/auth/adwords'],
  'googleads'
)

// 使用JWT向Google Ads Manager认证
const authenticationResult = await authenticateToGoogleAdsManager(
  serviceAccount.client_id,
  serviceAccount.private_key,
  jwt
)

console.log(authenticationResult)

// 使用access token发送点击转化数据
const test = await sendClickConversionToGoogleAdsManager(
  CUSTOMERID,
  clickConversion,
  accessToken.access_token,
  'DEV-TOKEN'
)

函数实现

/**
 * 为服务账户生成JSON Web Token (JWT)
 *
 * @param serviceAccount - 包含客户端邮箱和私钥的服务账户对象
 * @param scopes - 令牌授权的权限范围数组
 * @param serviceName - 令牌授权的服务名称,默认值为'oauth2'
 * @param expirationTimeInSeconds - 令牌过期时间(秒),默认3600秒(1小时)
 * @returns 生成的JSON Web Token
 */
export function generateJsonWebTokenForServiceAccount(
  serviceAccount: ServiceAccount,
  scopes: string[],
  serviceName: string = 'oauth2',
  expirationTimeInSeconds = 3600
) {
  const aud =
    serviceName === 'oauth2' ? 'https://oauth2.googleapis.com/token' : `https://${serviceName}.googleapis.com/`
  const currentTimestamp = Math.floor(Date.now() / 1000)
  const expirationTimestamp = currentTimestamp + expirationTimeInSeconds

  const payload = {
    iss: serviceAccount.client_email,
    sub: serviceAccount.client_email,
    scope: scopes.join(' '),
    aud: aud,
    exp: expirationTimestamp,
    iat: currentTimestamp
  }

  const options: SignOptions = {
    algorithm: 'RS256'
  }

  return jwt.sign(payload, serviceAccount.private_key, options)
}

/**
 * 使用提供的凭证向Google Ads Manager认证
 * @param clientId 认证用的客户端ID
 * @param clientSecret 认证用的客户端密钥
 * @param refreshToken 认证用的刷新令牌
 * @returns 解析为access token的Promise
 */
export async function authenticateToGoogleAdsManager(
  clientId: string,
  clientSecret: string,
  refreshToken: string
): Promise<string> {
  const url = 'https://www.googleapis.com/oauth2/v3/token'
  const body = {
    client_id: clientId,
    client_secret: clientSecret,
    refresh_token: refreshToken,
    grant_type: 'refresh_token'
  }

  const response = await fetch(url, {
    method: 'POST',
    body: JSON.stringify(body)
  })

  const data = await response.json()

  return data.access_token
}

/**
 * 向Google Ads Manager发送点击转化数据
 * 
 * @param customerId 客户ID
 * @param clickConversion 点击转化数据
 * @param accessToken 认证用的access token
 * @param developerToken 认证用的开发者令牌
 * @param options 可选的API配置项
 * @returns 解析为响应数据的Promise
 */
export async function sendClickConversionToGoogleAdsManager(
  customerId: number,
  clickConversion: ClickConversion,
  accessToken: string,
  developerToken: string,
  options?: ApiOptions
): Promise<any> {
  const url = `https://googleads.googleapis.com/v15/customers/${customerId}:uploadClickConversions`

  if (!options) {
    options = {
      partialFailure: false,
      validateOnly: false,
      debugEnabled: false,
      jobId: 0
    }
  }

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`,
      'developer-token': developerToken
    },
    body: JSON.stringify({
      conversions: [clickConversion],
      partialFailure: options.partialFailure,
      validateOnly: options.validateOnly,
      debugEnabled: options.debugEnabled,
      jobId: options.jobId
    })
  })

  const data = await response.json()
  return data
}

内容的提问来源于stack exchange,提问作者Björn Ammon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 05:35:16