Google Ads API via HTTP Request node: how to inject the developer-token header without hardcoding it?

Hi all,

I’m calling the Google Ads API from the HTTP Request node because the native Google Ads node only supports “Get campaigns” and I need other endpoints.

My setup:

  • HTTP Request node
  • Authentication → Predefined Credential Type → Google Ads OAuth2 API (this handles the OAuth Bearer token fine)

The problem: the call fails with DEVELOPER_TOKEN_PARAMETER_MISSING. The HTTP Request node does not automatically inject the developer-token header from the Google Ads OAuth2 credential, so I have to add it manually.

What I tried: I added a manual header developer-token and set the value with an expression so the token stays out of the workflow JSON:

={{ $credentials.developerToken }}

This does not work for me. The expression seems to resolve to empty, so the token is still missing and I get the same error. My guess is that $credentials is not exposed inside HTTP Request node expressions for security reasons, but I’m not sure.

My questions:

  1. Is there a supported way to reference the developer-token from the existing Google Ads OAuth2 credential inside the HTTP Request node, without typing the token inline? Right now I’m storing it in an $env, but I want to move away from that.
  2. If $credentials is not available there, what is the recommended approach to keep the token out of workflow exports?

I’m on a self-hosted instance (Elestio). Thanks in advance for any pointers.

@jochem youre right, $credentials isnt exposed in node expressions (only inside credential fields), so that header resolves empty, theres no supported way to pull the dev token from the OAuth2 cred into the HTTP node. keep OAuth2 for auth and set the developer-token header value to a variable instead, {{ $vars.googleAdsDevToken }} on cloud/enterprise (Variables feature) or {{ $env.GOOGLE_ADS_DEV_TOKEN }} self-hosted, so the token stays out of the workflow JSON.

The $credentials shorthand is not accessible in HTTP Request header value expressions. That object is only exposed inside custom credential type definitions, not in node fields.

For n8n Cloud, the cleanest approach is n8n Variables (Settings > Variables). Create a variable (e.g. GOOGLE_ADS_DEV_TOKEN) and store your developer token there. Then in the HTTP Request header value field use:

{{ $vars.GOOGLE_ADS_DEV_TOKEN }}

Variables are workspace-scoped, never appear in exported workflow JSON, and are available on Starter plan and above on Cloud.

If you are self-hosted, the alternative is an environment variable. Set GOOGLE_ADS_DEV_TOKEN=xyz in your Docker env and reference it in the header as:

{{ $env.GOOGLE_ADS_DEV_TOKEN }}

$env access requires N8N_BLOCK_ENV_ACCESS_IN_NODE=false (the default) to be active. On Cloud you cannot set env vars, so Variables is the right path.

Also worth checking: the Google Ads API often requires a login-customer-id header (your MCC customer ID, no dashes) on sub-account-scoped endpoints. Missing that is a common second cause of errors alongside the developer token.

Hi both, thank you for your quick responses. It does indeed work on self-hosted using an $env variable. I have always used this approach until now.

I recently upgraded to n8n v2 and get the impression that using the $env variable is discouraged due to security reasons. That’s why I was curious to know if anyone knows of a better alternative for self-hosted environments.

For now, I will proceed with N8N_BLOCK_ENV_ACCESS_IN_NODE=false.

I would be glad to hear of any better alternatives :slight_smile:

@jochem $env with N8N_BLOCK_ENV_ACCESS_IN_NODE=false is genuinely the intended approach on community self-hosted, the default block is just a guard against workflows accidentally reading host env vars, deliberately allowing it for one known secret isnt wrong. the only more-secure built-in option is External Secrets ($secrets, integrates with Vault / AWS / Azure / GCP secrets managers), but thats Enterprise self-hosted only. so on community theres no better alternative, youre fine sticking with the $env approach.

You’ve diagnosed it correctly — $credentials is intentionally not exposed in node expressions, so {{
$credentials.developerToken }} resolves to empty. And the HTTP Request node only injects the OAuth2 Bearer from the Google Ads credential, not the developer-token field — there’s no toggle to pass it through, and you can’t attach a second predefined credential. So the token has to come from somewhere referenceable.

Options to keep it out of the workflow export:

  • $env is actually a supported approach — it does keep the token out of the workflow JSON, so staying there isn’t wrong, just less convenient.
  • n8n Variables ({{ $vars.googleAdsDeveloperToken }}) — same effect but managed in the UI instead of env, if your plan has Variables.
  • The proper one-credential fix: build a small custom credential type that extends the Google Ads OAuth2 credential to also inject developer-token (and login-customer-id) as a header in its authenticate block. Then a single Predefined Credential handles Bearer and dev-token, the HTTP Request node needs no manual header, and nothing lands in the export. You’re self-hosted on Elestio, so you can drop a custom credential in — it’s the cleanest long-term answer and gives you exactly the “reference it from the credential, no inline” behavior you’re after.

Answer: No, there is no supported way to access credential fields from within HTTP Request node expressions. Credentials are used only for authentication (OAuth2) and are not exposed to expressions, so you cannot write:Answer: No, there is no supported way to access credential fields from within HTTP Request node expressions. Credentials are used only for authentication (OAuth2) and are not exposed to expressions, so you cannot write: